Compare commits

..

2 commits

Author SHA1 Message Date
io
42b8180f5e Sync da MacBook: 20260314_235447 2026-03-14 23:54:48 +01:00
dfe392ea88 Auto-sync: 20260304_142715 2026-03-04 14:27:15 +01:00
115 changed files with 2971 additions and 210198 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -1,3 +0,0 @@
index.html,1773586765860,5737ce966fa8786becaf7f36a32992cf44102fb3a217c226c30576c993b33e63
404.html,1773344753356,05cbc6f94d7a69ce2e29646eab13be2c884e61ba93e3094df5028866876d18b3
report.html,1774225497103,87e2cc9055f15faf5a6228e0933ea51a1fb147cecdcfd3336df8299474f0126e

View file

@ -1,5 +0,0 @@
{
"projects": {
"default": "tetraq-32a4a"
}
}

View file

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited. # This file should be version controlled and should not be manually edited.
version: version:
revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6" revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
channel: "stable" channel: "stable"
project_type: app project_type: app
@ -13,17 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: android
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: ios
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: macos - platform: macos
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
# User provided section # User provided section

View file

@ -8,19 +8,8 @@ plugins {
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
// Aggiungiamo esplicitamente gli import richiesti da Kotlin
import java.io.FileInputStream
import java.util.Properties
// Carichiamo il file con le password
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android { android {
namespace = "com.amastra.tetraq" namespace = "com.sanza.tetraq"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
@ -29,16 +18,13 @@ android {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
// Sintassi aggiornata come richiesto dal compilatore Kotlin kotlinOptions {
kotlin { jvmTarget = JavaVersion.VERSION_17.toString()
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.amastra.tetraq" applicationId = "com.sanza.tetraq"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
@ -47,28 +33,15 @@ android {
versionName = flutter.versionName versionName = flutter.versionName
} }
// Aggiunto il blocco per la firma in formato Kotlin DSL
signingConfigs {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
val storeFileString = keystoreProperties.getProperty("storeFile")
if (storeFileString != null) {
storeFile = file(storeFileString)
}
storePassword = keystoreProperties.getProperty("storePassword")
}
}
buildTypes { buildTypes {
getByName("release") { release {
// TODO: Add your own signing config for the release build. // TODO: Add your own signing config for the release build.
// Ora usiamo la chiave di release appena creata // Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("debug")
} }
} }
} }
flutter { flutter {
source = "../.." source = "../.."
} }

View file

@ -5,25 +5,6 @@
"storage_bucket": "tetraq-32a4a.firebasestorage.app" "storage_bucket": "tetraq-32a4a.firebasestorage.app"
}, },
"client": [ "client": [
{
"client_info": {
"mobilesdk_app_id": "1:705460445314:android:ceac21bb06b7a9f07b949b",
"android_client_info": {
"package_name": "com.amastra.tetraq"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyBsXO595xVITDPrRnXrW8HPQLOe7Rz4Gg4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{ {
"client_info": { "client_info": {
"mobilesdk_app_id": "1:705460445314:android:4d35fef29cfd63727b949b", "mobilesdk_app_id": "1:705460445314:android:4d35fef29cfd63727b949b",

View file

@ -5,31 +5,31 @@
<application <application
android:label="tetraq" android:label="tetraq"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher">
android:usesCleartextTraffic="true"> <activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tetraq" android:host="join" /> <data android:scheme="tetraq" android:host="join" />
</intent-filter> </intent-filter>
</activity> </activity>
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />

View file

@ -1,4 +1,4 @@
package com.amastra.tetraq package com.sanza.tetraq
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

BIN
assets/.DS_Store vendored

Binary file not shown.

BIN
assets/audio/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View file

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

BIN
assets/images/wood_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -1,47 +1 @@
{ {"flutter":{"platforms":{"android":{"default":{"projectId":"tetraq-32a4a","appId":"1:705460445314:android:4d35fef29cfd63727b949b","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"tetraq-32a4a","appId":"1:705460445314:ios:da11cbca5d1f6bc27b949b","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"tetraq-32a4a","appId":"1:705460445314:ios:da11cbca5d1f6bc27b949b","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"tetraq-32a4a","configurations":{"android":"1:705460445314:android:4d35fef29cfd63727b949b","ios":"1:705460445314:ios:da11cbca5d1f6bc27b949b","macos":"1:705460445314:ios:da11cbca5d1f6bc27b949b"}}}}}}
"flutter": {
"platforms": {
"android": {
"default": {
"projectId": "tetraq-32a4a",
"appId": "1:705460445314:android:ceac21bb06b7a9f07b949b",
"fileOutput": "android/app/google-services.json"
}
},
"ios": {
"default": {
"projectId": "tetraq-32a4a",
"appId": "1:705460445314:ios:54d64cb7592954327b949b",
"uploadDebugSymbols": false,
"fileOutput": "ios/Runner/GoogleService-Info.plist"
}
},
"macos": {
"default": {
"projectId": "tetraq-32a4a",
"appId": "1:705460445314:ios:da11cbca5d1f6bc27b949b",
"uploadDebugSymbols": false,
"fileOutput": "macos/Runner/GoogleService-Info.plist"
}
},
"dart": {
"lib/firebase_options.dart": {
"projectId": "tetraq-32a4a",
"configurations": {
"android": "1:705460445314:android:ceac21bb06b7a9f07b949b",
"ios": "1:705460445314:ios:54d64cb7592954327b949b",
"macos": "1:705460445314:ios:da11cbca5d1f6bc27b949b"
}
}
}
}
},
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}

View file

@ -1,27 +0,0 @@
import 'dart:io';
import 'dart:convert';
void main() async {
final dir = Directory('lib/l10n');
if (!await dir.exists()) await dir.create(recursive: true);
final Map<String, Map<String, String>> translations = {
'it': {"appTitle": "TetraQ", "welcomeTitle": "BENVENUTO IN TETRAQ!", "nameHint": "NOME", "saveAndPlay": "SALVA E GIOCA", "onlineTitle": "ONLINE", "onlineSub": "Sfida il mondo", "cpuTitle": "VS CPU", "cpuSub": "Allenati con l'IA", "localTitle": "LOCALE", "localSub": "Stesso schermo", "leaderboardTitle": "CLASSIFICA", "questsTitle": "SFIDE", "themesTitle": "TEMI", "tutorialTitle": "TUTORIAL", "startGame": "AVVIA PARTITA", "createMatch": "CREA PARTITA", "joinMatch": "UNISCITI", "gameOver": "FINE PARTITA", "mainMenu": "TORNA AL MENU", "exit": "ESCI"},
'en': {"appTitle": "TetraQ", "welcomeTitle": "WELCOME TO TETRAQ!", "nameHint": "NAME", "saveAndPlay": "SAVE & PLAY", "onlineTitle": "ONLINE", "onlineSub": "Challenge the world", "cpuTitle": "VS CPU", "cpuSub": "Train with AI", "localTitle": "LOCAL", "localSub": "Same screen", "leaderboardTitle": "LEADERBOARD", "questsTitle": "QUESTS", "themesTitle": "THEMES", "tutorialTitle": "TUTORIAL", "startGame": "START GAME", "createMatch": "CREATE MATCH", "joinMatch": "JOIN", "gameOver": "GAME OVER", "mainMenu": "BACK TO MENU", "exit": "EXIT"},
'es': {"appTitle": "TetraQ", "welcomeTitle": "¡BIENVENIDO A TETRAQ!", "nameHint": "NOMBRE", "saveAndPlay": "GUARDAR Y JUGAR", "onlineTitle": "ONLINE", "onlineSub": "Desafía al mundo", "cpuTitle": "VS CPU", "cpuSub": "Entrena con IA", "localTitle": "LOCAL", "localSub": "Misma pantalla", "leaderboardTitle": "RANKING", "questsTitle": "MISIONES", "themesTitle": "TEMAS", "tutorialTitle": "TUTORIAL", "startGame": "INICIAR JUEGO", "createMatch": "CREAR PARTIDA", "joinMatch": "UNIRSE", "gameOver": "FIN DEL JUEGO", "mainMenu": "VOLVER AL MENÚ", "exit": "SALIR"},
'fr': {"appTitle": "TetraQ", "welcomeTitle": "BIENVENUE DANS TETRAQ !", "nameHint": "NOM", "saveAndPlay": "SAUVEGARDER ET JOUER", "onlineTitle": "EN LIGNE", "onlineSub": "Défiez le monde", "cpuTitle": "VS CPU", "cpuSub": "Entraînez avec l'IA", "localTitle": "LOCAL", "localSub": "Même écran", "leaderboardTitle": "CLASSEMENT", "questsTitle": "QUÊTES", "themesTitle": "THÈMES", "tutorialTitle": "TUTORIEL", "startGame": "JOUER", "createMatch": "CRÉER UN MATCH", "joinMatch": "REJOINDRE", "gameOver": "FIN DE PARTIE", "mainMenu": "RETOUR AU MENU", "exit": "QUITTER"},
'de': {"appTitle": "TetraQ", "welcomeTitle": "WILLKOMMEN BEI TETRAQ!", "nameHint": "NAME", "saveAndPlay": "SPEICHERN & SPIELEN", "onlineTitle": "ONLINE", "onlineSub": "Fordere die Welt heraus", "cpuTitle": "VS CPU", "cpuSub": "Trainiere mit KI", "localTitle": "LOKAL", "localSub": "Gleicher Bildschirm", "leaderboardTitle": "RANGLISTE", "questsTitle": "MISSIONEN", "themesTitle": "THEMEN", "tutorialTitle": "TUTORIAL", "startGame": "SPIEL STARTEN", "createMatch": "SPIEL ERSTELLEN", "joinMatch": "BEITRETEN", "gameOver": "SPIELENDE", "mainMenu": "ZURÜCK ZUM MENÜ", "exit": "BEENDEN"},
'pt': {"appTitle": "TetraQ", "welcomeTitle": "BEM-VINDO AO TETRAQ!", "nameHint": "NOME", "saveAndPlay": "SALVAR E JOGAR", "onlineTitle": "ONLINE", "onlineSub": "Desafie o mundo", "cpuTitle": "VS CPU", "cpuSub": "Treine com a IA", "localTitle": "LOCAL", "localSub": "Mesma tela", "leaderboardTitle": "CLASSIFICAÇÃO", "questsTitle": "DESAFIOS", "themesTitle": "TEMAS", "tutorialTitle": "TUTORIAL", "startGame": "INICIAR JOGO", "createMatch": "CRIAR PARTIDA", "joinMatch": "ENTRAR", "gameOver": "FIM DE JOGO", "mainMenu": "VOLTAR AO MENU", "exit": "SAIR"},
'ru': {"appTitle": "TetraQ", "welcomeTitle": "ДОБРО ПОЖАЛОВАТЬ В TETRAQ!", "nameHint": "ИМЯ", "saveAndPlay": "СОХРАНИТЬ И ИГРАТЬ", "onlineTitle": "ОНЛАЙН", "onlineSub": "Брось вызов миру", "cpuTitle": "VS ИИ", "cpuSub": "Тренировка с ИИ", "localTitle": "ЛОКАЛЬНО", "localSub": "Один экран", "leaderboardTitle": "РЕЙТИНГ", "questsTitle": "ЗАДАНИЯ", "themesTitle": "ТЕМЫ", "tutorialTitle": "ОБУЧЕНИЕ", "startGame": "НАЧАТЬ ИГРУ", "createMatch": "СОЗДАТЬ ИГРУ", "joinMatch": "ПРИСОЕДИНИТЬСЯ", "gameOver": "ИГРА ОКОНЧЕНА", "mainMenu": "В ГЛАВНОЕ МЕНЮ", "exit": "ВЫХОД"},
'zh': {"appTitle": "TetraQ", "welcomeTitle": "欢迎来到 TETRAQ", "nameHint": "名字", "saveAndPlay": "保存并开始", "onlineTitle": "在线匹配", "onlineSub": "挑战世界", "cpuTitle": "人机对战", "cpuSub": "与AI训练", "localTitle": "本地游戏", "localSub": "同屏对战", "leaderboardTitle": "排行榜", "questsTitle": "任务", "themesTitle": "主题", "tutorialTitle": "教程", "startGame": "开始游戏", "createMatch": "创建比赛", "joinMatch": "加入", "gameOver": "游戏结束", "mainMenu": "返回主菜单", "exit": "退出"}
};
for (var lang in translations.keys) {
final file = File('lib/l10n/app_$lang.arb');
final Map<String, dynamic> finalContent = {"@@locale": lang, ...translations[lang]!};
await file.writeAsString(JsonEncoder.withIndent(' ').convert(finalContent));
}
// Crea anche il file di configurazione
await File('l10n.yaml').writeAsString("arb-dir: lib/l10n\ntemplate-arb-file: app_it.arb\noutput-localization-file: app_localizations.dart\n");
}

BIN
ios/.DS_Store vendored

Binary file not shown.

View file

@ -1190,10 +1190,6 @@ PODS:
- abseil/xcprivacy (1.20240722.0) - abseil/xcprivacy (1.20240722.0)
- app_links (7.0.0): - app_links (7.0.0):
- Flutter - Flutter
- AppCheckCore (11.2.0):
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- audioplayers_darwin (0.0.1): - audioplayers_darwin (0.0.1):
- Flutter - Flutter
- BoringSSL-GRPC (0.0.37): - BoringSSL-GRPC (0.0.37):
@ -1203,62 +1199,32 @@ PODS:
- BoringSSL-GRPC/Interface (= 0.0.37) - BoringSSL-GRPC/Interface (= 0.0.37)
- BoringSSL-GRPC/Interface (0.0.37) - BoringSSL-GRPC/Interface (0.0.37)
- cloud_firestore (6.1.2): - cloud_firestore (6.1.2):
- Firebase/Firestore (= 12.9.0) - Firebase/Firestore (= 12.8.0)
- firebase_core - firebase_core
- Flutter - Flutter
- device_info_plus (0.0.1): - Firebase/CoreOnly (12.8.0):
- Flutter - FirebaseCore (~> 12.8.0)
- Firebase/Auth (12.9.0): - Firebase/Firestore (12.8.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAuth (~> 12.9.0) - FirebaseFirestore (~> 12.8.0)
- Firebase/CoreOnly (12.9.0): - firebase_core (4.4.0):
- FirebaseCore (~> 12.9.0) - Firebase/CoreOnly (= 12.8.0)
- Firebase/Firestore (12.9.0):
- Firebase/CoreOnly
- FirebaseFirestore (~> 12.9.0)
- firebase_app_check (0.4.1-5):
- Firebase/CoreOnly (~> 12.9.0)
- firebase_core
- FirebaseAppCheck (~> 12.9.0)
- Flutter - Flutter
- firebase_auth (6.1.4): - FirebaseAppCheckInterop (12.8.0)
- Firebase/Auth (= 12.9.0) - FirebaseCore (12.8.0):
- firebase_core - FirebaseCoreInternal (~> 12.8.0)
- Flutter
- firebase_core (4.5.0):
- Firebase/CoreOnly (= 12.9.0)
- Flutter
- FirebaseAppCheck (12.9.0):
- AppCheckCore (~> 11.0)
- FirebaseAppCheckInterop (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- FirebaseAppCheckInterop (12.9.0)
- FirebaseAuth (12.9.0):
- FirebaseAppCheckInterop (~> 12.9.0)
- FirebaseAuthInterop (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- FirebaseCoreExtension (~> 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GTMSessionFetcher/Core (< 6.0, >= 3.4)
- RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (12.9.0)
- FirebaseCore (12.9.0):
- FirebaseCoreInternal (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1) - GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.9.0): - FirebaseCoreExtension (12.8.0):
- FirebaseCore (~> 12.9.0) - FirebaseCore (~> 12.8.0)
- FirebaseCoreInternal (12.9.0): - FirebaseCoreInternal (12.8.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseFirestore (12.9.0): - FirebaseFirestore (12.8.0):
- FirebaseCore (~> 12.9.0) - FirebaseCore (~> 12.8.0)
- FirebaseCoreExtension (~> 12.9.0) - FirebaseCoreExtension (~> 12.8.0)
- FirebaseFirestoreInternal (~> 12.9.0) - FirebaseFirestoreInternal (~> 12.8.0)
- FirebaseSharedSwift (~> 12.9.0) - FirebaseSharedSwift (~> 12.8.0)
- FirebaseFirestoreInternal (12.9.0): - FirebaseFirestoreInternal (12.8.0):
- abseil/algorithm (~> 1.20240722.0) - abseil/algorithm (~> 1.20240722.0)
- abseil/base (~> 1.20240722.0) - abseil/base (~> 1.20240722.0)
- abseil/container/flat_hash_map (~> 1.20240722.0) - abseil/container/flat_hash_map (~> 1.20240722.0)
@ -1267,38 +1233,22 @@ PODS:
- abseil/strings/strings (~> 1.20240722.0) - abseil/strings/strings (~> 1.20240722.0)
- abseil/time (~> 1.20240722.0) - abseil/time (~> 1.20240722.0)
- abseil/types (~> 1.20240722.0) - abseil/types (~> 1.20240722.0)
- FirebaseAppCheckInterop (~> 12.9.0) - FirebaseAppCheckInterop (~> 12.8.0)
- FirebaseCore (~> 12.9.0) - FirebaseCore (~> 12.8.0)
- "gRPC-C++ (~> 1.69.0)" - "gRPC-C++ (~> 1.69.0)"
- gRPC-Core (~> 1.69.0) - gRPC-Core (~> 1.69.0)
- leveldb-library (~> 1.22) - leveldb-library (~> 1.22)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseSharedSwift (12.9.0) - FirebaseSharedSwift (12.8.0)
- Flutter (1.0.0) - Flutter (1.0.0)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0): - GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0): - GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment - GoogleUtilities/Environment
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)": - "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0) - GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- "gRPC-C++ (1.69.0)": - "gRPC-C++ (1.69.0)":
- "gRPC-C++/Implementation (= 1.69.0)" - "gRPC-C++/Implementation (= 1.69.0)"
- "gRPC-C++/Interface (= 1.69.0)" - "gRPC-C++/Interface (= 1.69.0)"
@ -1391,49 +1341,33 @@ PODS:
- gRPC-Core/Privacy (= 1.69.0) - gRPC-Core/Privacy (= 1.69.0)
- gRPC-Core/Interface (1.69.0) - gRPC-Core/Interface (1.69.0)
- gRPC-Core/Privacy (1.69.0) - gRPC-Core/Privacy (1.69.0)
- GTMSessionFetcher/Core (5.2.0)
- leveldb-library (1.22.6) - leveldb-library (1.22.6)
- nanopb (3.30910.0): - nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0) - nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0) - nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0) - nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0) - nanopb/encode (3.30910.0)
- package_info_plus (0.4.5):
- Flutter
- PromisesObjC (2.4.0)
- RecaptchaInterop (101.0.0)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES: 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_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- abseil - abseil
- AppCheckCore
- BoringSSL-GRPC - BoringSSL-GRPC
- Firebase - Firebase
- FirebaseAppCheck
- FirebaseAppCheckInterop - FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore - FirebaseCore
- FirebaseCoreExtension - FirebaseCoreExtension
- FirebaseCoreInternal - FirebaseCoreInternal
@ -1443,11 +1377,8 @@ SPEC REPOS:
- GoogleUtilities - GoogleUtilities
- "gRPC-C++" - "gRPC-C++"
- gRPC-Core - gRPC-Core
- GTMSessionFetcher
- leveldb-library - leveldb-library
- nanopb - nanopb
- PromisesObjC
- RecaptchaInterop
EXTERNAL SOURCES: EXTERNAL SOURCES:
app_links: app_links:
@ -1456,60 +1387,38 @@ 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:
:path: ".symlinks/plugins/firebase_app_check/ios"
firebase_auth:
:path: ".symlinks/plugins/firebase_auth/ios"
firebase_core: firebase_core:
:path: ".symlinks/plugins/firebase_core/ios" :path: ".symlinks/plugins/firebase_core/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
abseil: a05cc83bf02079535e17169a73c5be5ba47f714b abseil: a05cc83bf02079535e17169a73c5be5ba47f714b
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8 app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab
BoringSSL-GRPC: dded2a44897e45f28f08ae87a55ee4bcd19bc508 BoringSSL-GRPC: dded2a44897e45f28f08ae87a55ee4bcd19bc508
cloud_firestore: 81f6c428ecee874dc3808afe0e0c48a87beb5bdf cloud_firestore: 4bd00c3464706d9e09dabac0bb8e9610456109f5
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56 firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398
firebase_app_check: 33f1df6830ec8ebadee0db0120956c44a65c7213 FirebaseAppCheckInterop: ba3dc604a89815379e61ec2365101608d365cf7d
firebase_auth: fecf9fe293464b52063f5f2a7110e63ff2ab3403 FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c
firebase_core: afac1aac13c931e0401c7e74ed1276112030efab FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2
FirebaseAppCheck: 94dae4d9bb682bdef85a778b0c1024a4613f1e89 FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21
FirebaseAppCheckInterop: 4bade10286cc977e516f75d2d8312cbdfa534789 FirebaseFirestore: 67f23000ca238ccbab79127ed59636a9a2689e74
FirebaseAuth: 3a39f6436c21ebfd7919b698228b4f89ff94c23b FirebaseFirestoreInternal: a0e7382af3d208898dcd1d4d52d8a7870632e881
FirebaseAuthInterop: f8f6ff72dc24621906497fbe5cf3c42ee815e59c FirebaseSharedSwift: f57ed48f4542b2d7eb4738f4f23ba443f78b3780
FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
FirebaseCoreExtension: e911052d59cd0da237a45d706fc0f81654f035c1
FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
FirebaseFirestore: d8b76ca1feb4ca0b0f078c45f7d1bd8014a49ef1
FirebaseFirestoreInternal: 02341a9ba87f6309227b04685022a5e16307bbf7
FirebaseSharedSwift: 9d2fa84a46676302b89dbd5e6e62bce2fe376909
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
"gRPC-C++": cc207623316fb041a7a3e774c252cf68a058b9e8 "gRPC-C++": cc207623316fb041a7a3e774c252cf68a058b9e8
gRPC-Core: 860978b7db482de8b4f5e10677216309b5ff6330 gRPC-Core: 860978b7db482de8b4f5e10677216309b5ff6330
GTMSessionFetcher: 904bdd2a82c635bcd6f44edf94cc8775c5d1d6e6
leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
PODFILE CHECKSUM: 3d68f7cb47d5f2fb7765407f663653c9b51100f3 PODFILE CHECKSUM: 3d68f7cb47d5f2fb7765407f663653c9b51100f3

View file

@ -52,7 +52,6 @@
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4867B86862DC650EC26D5F9C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4867B86862DC650EC26D5F9C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
52CB81B72F635109004C3F43 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
5C30E1EF56D9EC1CAEADBE23 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; 5C30E1EF56D9EC1CAEADBE23 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -147,7 +146,6 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
52CB81B72F635109004C3F43 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@ -475,9 +473,6 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = 2BX6QRR7GG; DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@ -490,7 +485,6 @@
MARKETING_VERSION = 1.0.2; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@ -506,7 +500,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.amastra.tetraq.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -524,7 +518,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.amastra.tetraq.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -540,7 +534,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.amastra.tetraq.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -664,9 +658,6 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = 2BX6QRR7GG; DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@ -679,7 +670,6 @@
MARKETING_VERSION = 1.0.2; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -693,9 +683,6 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = 2BX6QRR7GG; DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@ -708,7 +695,6 @@
MARKETING_VERSION = 1.0.2; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";

View file

@ -73,12 +73,6 @@
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "-FIRDebugEnabled"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Profile" buildConfiguration = "Profile"

View file

@ -9,7 +9,7 @@
<key>PLIST_VERSION</key> <key>PLIST_VERSION</key>
<string>1</string> <string>1</string>
<key>BUNDLE_ID</key> <key>BUNDLE_ID</key>
<string>com.amastra.tetraq</string> <string>com.sanza.tetraq</string>
<key>PROJECT_ID</key> <key>PROJECT_ID</key>
<string>tetraq-32a4a</string> <string>tetraq-32a4a</string>
<key>STORAGE_BUCKET</key> <key>STORAGE_BUCKET</key>
@ -25,6 +25,6 @@
<key>IS_SIGNIN_ENABLED</key> <key>IS_SIGNIN_ENABLED</key>
<true></true> <true></true>
<key>GOOGLE_APP_ID</key> <key>GOOGLE_APP_ID</key>
<string>1:705460445314:ios:54d64cb7592954327b949b</string> <string>1:705460445314:ios:da11cbca5d1f6bc27b949b</string>
</dict> </dict>
</plist> </plist>

View file

@ -2,67 +2,48 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CFBundleDevelopmentRegion</key>
<true/> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDisplayName</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>Tetraq</string>
<key>CFBundleDisplayName</key> <key>CFBundleExecutable</key>
<string>Tetraq</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleExecutable</key> <key>CFBundleIdentifier</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleIdentifier</key> <key>CFBundleInfoDictionaryVersion</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>6.0</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleName</key>
<string>6.0</string> <string>tetraq</string>
<key>CFBundleName</key> <key>CFBundlePackageType</key>
<string>tetraq</string> <string>APPL</string>
<key>CFBundlePackageType</key> <key>CFBundleShortVersionString</key>
<string>APPL</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleSignature</key>
<string>$(FLUTTER_BUILD_NAME)</string> <string>????</string>
<key>CFBundleSignature</key> <key>CFBundleVersion</key>
<string>????</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>CFBundleURLTypes</key> <key>LSRequiresIPhoneOS</key>
<array> <true/>
<dict> <key>UILaunchStoryboardName</key>
<key>CFBundleTypeRole</key> <string>LaunchScreen</string>
<string>Editor</string> <key>UIMainStoryboardFile</key>
<key>CFBundleURLName</key> <string>Main</string>
<string>com.sanza.tetraq</string> <key>UISupportedInterfaceOrientations</key>
<key>CFBundleURLSchemes</key> <array>
<array> <string>UIInterfaceOrientationPortrait</string>
<string>tetraq</string> <string>UIInterfaceOrientationLandscapeLeft</string>
</array> <string>UIInterfaceOrientationLandscapeRight</string>
</dict> </array>
</array> <key>UISupportedInterfaceOrientations~ipad</key>
<key>CFBundleVersion</key> <array>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>UIInterfaceOrientationPortrait</string>
<key>LSRequiresIPhoneOS</key> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<true/> <string>UIInterfaceOrientationLandscapeLeft</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <string>UIInterfaceOrientationLandscapeRight</string>
<true/> </array>
<key>UILaunchStoryboardName</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<string>LaunchScreen</string> <true/>
<key>UIMainStoryboardFile</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<string>Main</string> <true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict> </dict>
</plist> </plist>

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:tetraq-32a4a.web.app</string>
</array>
</dict>
</plist>

View file

@ -1,4 +0,0 @@
arb-dir: lib/l10n
template-arb-file: app_it.arb
output-localization-file: app_localizations.dart
output-dir: lib/l10n

BIN
lib/.DS_Store vendored

Binary file not shown.

View file

@ -5,7 +5,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
enum AppThemeType { doodle, cyberpunk, arcade, grimorio, music } enum AppThemeType { minimal, doodle, cyberpunk, wood, arcade, grimorio }
class ThemeColors { class ThemeColors {
final Color background; final Color background;
@ -24,6 +24,11 @@ class ThemeColors {
} }
class AppColors { class AppColors {
static const ThemeColors minimal = ThemeColors(
background: Color(0xFFF5F7FA), gridLine: Color(0xFFCFD8DC),
playerRed: Color(0xFFE53935), playerBlue: Color(0xFF1E88E5), text: Color(0xFF263238),
);
static const ThemeColors doodle = ThemeColors( static const ThemeColors doodle = ThemeColors(
background: Color(0xFFFFF9E6), gridLine: Color(0xFFB0BEC5), background: Color(0xFFFFF9E6), gridLine: Color(0xFFB0BEC5),
playerRed: Color(0xFFD32F2F), playerBlue: Color(0xFF1565C0), text: Color(0xFF37474F), playerRed: Color(0xFFD32F2F), playerBlue: Color(0xFF1565C0), text: Color(0xFF37474F),
@ -34,31 +39,29 @@ 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, background: Color(0xFF1E112A), gridLine: Color(0xFF8D6E63),
playerRed: Color(0xFFE91E63), playerBlue: Color(0xFF4FC3F7), text: Color(0xFFFFF3E0), playerRed: Color(0xFFE91E63), playerBlue: Color(0xFF4FC3F7), text: Color(0xFFFFF3E0),
); );
static const ThemeColors music = ThemeColors(
background: Color(0xFF120B29),
gridLine: Color(0xFF6A1B9A),
playerRed: Color(0xFFFF2A6D),
playerBlue: Color(0xFF05D5FF),
text: Color(0xFFE0E0E0),
);
static ThemeColors getTheme(AppThemeType type) { static ThemeColors getTheme(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.minimal: return minimal;
case AppThemeType.doodle: return doodle; case AppThemeType.doodle: return doodle;
case AppThemeType.cyberpunk: return cyberpunk; case AppThemeType.cyberpunk: return cyberpunk;
case AppThemeType.wood: return wood;
case AppThemeType.arcade: return arcade; case AppThemeType.arcade: return arcade;
case AppThemeType.grimorio: return grimorio; case AppThemeType.grimorio: return grimorio;
case AppThemeType.music: return music;
} }
} }
} }
@ -66,61 +69,65 @@ class AppColors {
class ThemeIcons { class ThemeIcons {
static IconData gold(AppThemeType type) { static IconData gold(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.minimal: return Icons.star_rounded;
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;
case AppThemeType.music: return FontAwesomeIcons.compactDisc;
} }
} }
static IconData bomb(AppThemeType type) { static IconData bomb(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.minimal: return Icons.mood_bad_rounded;
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;
case AppThemeType.music: return FontAwesomeIcons.volumeXmark;
} }
} }
static IconData swap(AppThemeType type) { static IconData swap(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.minimal: return Icons.sync_rounded;
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;
case AppThemeType.music: return FontAwesomeIcons.sliders;
} }
} }
static IconData joker(AppThemeType type) { static IconData joker(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.minimal: return Icons.sentiment_satisfied_alt;
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;
case AppThemeType.music: return FontAwesomeIcons.headphones;
} }
} }
static IconData block(AppThemeType type) { static IconData block(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.minimal: return Icons.block;
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;
case AppThemeType.music: return FontAwesomeIcons.pause;
} }
} }
// --- NUOVE ICONE ---
static IconData ice(AppThemeType type) { static IconData ice(AppThemeType type) {
if (type == AppThemeType.music) return FontAwesomeIcons.music;
return FontAwesomeIcons.snowflake; return FontAwesomeIcons.snowflake;
} }
static IconData multiplier(AppThemeType type) { static IconData multiplier(AppThemeType type) {
if (type == AppThemeType.music) return FontAwesomeIcons.forwardFast;
return FontAwesomeIcons.bolt; return FontAwesomeIcons.bolt;
} }
} }

View file

@ -1,66 +1,21 @@
// ===========================================================================
// FILE: lib/core/theme_manager.dart
// ===========================================================================
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';
// --- ENUM DEI TEMI AGGIORNATO --- class ThemeManager extends ChangeNotifier {
const Map<AppThemeType, IconData> themeIcons = { late AppThemeType _currentThemeType;
AppThemeType.cyberpunk: Icons.electric_bolt,
AppThemeType.doodle: Icons.brush,
AppThemeType.music: Icons.headset_mic,
AppThemeType.arcade: Icons.videogame_asset,
AppThemeType.grimorio: Icons.auto_stories,
};
const Map<AppThemeType, String> themeNames = {
AppThemeType.cyberpunk: "Cyberpunk",
AppThemeType.doodle: "Doodle",
AppThemeType.music: "Music",
AppThemeType.arcade: "Arcade",
AppThemeType.grimorio: "Grimorio",
};
class ThemeManager with ChangeNotifier {
AppThemeType _currentThemeType = AppThemeType.doodle;
ThemeColors _currentColors = AppColors.getTheme(AppThemeType.doodle);
AppThemeType get currentThemeType => _currentThemeType;
ThemeColors get currentColors => _currentColors;
ThemeManager() { ThemeManager() {
_loadTheme(); // Quando l'app parte, legge il tema dalla memoria!
_currentThemeType = AppThemeType.values[StorageService.instance.savedThemeIndex];
} }
void _loadTheme() async { AppThemeType get currentThemeType => _currentThemeType;
String themeStr = StorageService.instance.getTheme(); ThemeColors get currentColors => AppColors.getTheme(_currentThemeType);
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;
_currentColors = AppColors.getTheme(type); StorageService.instance.saveTheme(type); // Salva la scelta nel "disco fisso"
StorageService.instance.saveTheme(type.toString());
_updateSystemUI();
notifyListeners(); notifyListeners();
} }
void _updateSystemUI() {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: _currentThemeType == AppThemeType.doodle ? Brightness.dark : Brightness.light,
systemNavigationBarColor: _currentColors.background,
systemNavigationBarIconBrightness: _currentThemeType == AppThemeType.doodle ? Brightness.dark : Brightness.light,
));
}
} }

View file

@ -48,7 +48,7 @@ class DefaultFirebaseOptions {
static const FirebaseOptions android = FirebaseOptions( static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyBsXO595xVITDPrRnXrW8HPQLOe7Rz4Gg4', apiKey: 'AIzaSyBsXO595xVITDPrRnXrW8HPQLOe7Rz4Gg4',
appId: '1:705460445314:android:ceac21bb06b7a9f07b949b', appId: '1:705460445314:android:4d35fef29cfd63727b949b',
messagingSenderId: '705460445314', messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a', projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app', storageBucket: 'tetraq-32a4a.firebasestorage.app',
@ -56,11 +56,11 @@ class DefaultFirebaseOptions {
static const FirebaseOptions ios = FirebaseOptions( static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyB77j18Jgeb9gBAEwp-uyOQvr4m-RJ_rAE', apiKey: 'AIzaSyB77j18Jgeb9gBAEwp-uyOQvr4m-RJ_rAE',
appId: '1:705460445314:ios:54d64cb7592954327b949b', appId: '1:705460445314:ios:da11cbca5d1f6bc27b949b',
messagingSenderId: '705460445314', messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a', projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app', storageBucket: 'tetraq-32a4a.firebasestorage.app',
iosBundleId: 'com.amastra.tetraq', iosBundleId: 'com.sanza.tetraq',
); );
static const FirebaseOptions macos = FirebaseOptions( static const FirebaseOptions macos = FirebaseOptions(
@ -71,5 +71,4 @@ class DefaultFirebaseOptions {
storageBucket: 'tetraq-32a4a.firebasestorage.app', storageBucket: 'tetraq-32a4a.firebasestorage.app',
iosBundleId: 'com.sanza.tetraq', iosBundleId: 'com.sanza.tetraq',
); );
}
}

View file

@ -1,23 +0,0 @@
{
"@@locale": "de",
"appTitle": "TetraQ",
"welcomeTitle": "WILLKOMMEN BEI TETRAQ!",
"nameHint": "NAME",
"saveAndPlay": "SPEICHERN & SPIELEN",
"onlineTitle": "ONLINE",
"onlineSub": "Fordere die Welt heraus",
"cpuTitle": "VS CPU",
"cpuSub": "Trainiere mit KI",
"localTitle": "LOKAL",
"localSub": "Gleicher Bildschirm",
"leaderboardTitle": "RANGLISTE",
"questsTitle": "MISSIONEN",
"themesTitle": "THEMEN",
"tutorialTitle": "TUTORIAL",
"startGame": "SPIEL STARTEN",
"createMatch": "SPIEL ERSTELLEN",
"joinMatch": "BEITRETEN",
"gameOver": "SPIELENDE",
"mainMenu": "ZURÜCK ZUM MENÜ",
"exit": "BEENDEN"
}

View file

@ -1,36 +1,4 @@
{ {
"@@locale": "en",
"appTitle": "TetraQ", "appTitle": "TetraQ",
"welcomeTitle": "WELCOME TO TETRAQ!", "playLocal": "PASS & PLAY (Local)"
"nameHint": "NAME", }
"saveAndPlay": "SAVE & PLAY",
"onlineTitle": "ONLINE",
"onlineSub": "Challenge the world",
"cpuTitle": "VS CPU",
"cpuSub": "Train with AI",
"localTitle": "LOCAL",
"localSub": "Same screen",
"leaderboardTitle": "LEADERBOARD",
"questsTitle": "QUESTS",
"themesTitle": "THEMES",
"tutorialTitle": "TUTORIAL",
"startGame": "START GAME",
"createMatch": "CREATE MATCH",
"joinMatch": "JOIN",
"gameOver": "GAME OVER",
"mainMenu": "BACK TO MENU",
"exit": "EXIT",
"roomSettings": "ROOM SETTINGS",
"arenaShape": "ARENA SHAPE",
"arenaSize": "SIZE",
"timeAndOptions": "TIME & OPTIONS",
"timeLabel": "TIME",
"btnStart": "START",
"btnCancel": "CANCEL",
"wordOr": "OR",
"codeHint": "CODE",
"publicLobbyTitle": "PUBLIC LOBBY",
"emptyLobbyMsg": "No public rooms right now.\nCreate one!",
"roomOf": "Room of",
"btnEnter": "ENTER"
}

View file

@ -1,23 +0,0 @@
{
"@@locale": "es",
"appTitle": "TetraQ",
"welcomeTitle": "¡BIENVENIDO A TETRAQ!",
"nameHint": "NOMBRE",
"saveAndPlay": "GUARDAR Y JUGAR",
"onlineTitle": "ONLINE",
"onlineSub": "Desafía al mundo",
"cpuTitle": "VS CPU",
"cpuSub": "Entrena con IA",
"localTitle": "LOCAL",
"localSub": "Misma pantalla",
"leaderboardTitle": "RANKING",
"questsTitle": "MISIONES",
"themesTitle": "TEMAS",
"tutorialTitle": "TUTORIAL",
"startGame": "INICIAR JUEGO",
"createMatch": "CREAR PARTIDA",
"joinMatch": "UNIRSE",
"gameOver": "FIN DEL JUEGO",
"mainMenu": "VOLVER AL MENÚ",
"exit": "SALIR"
}

View file

@ -1,23 +0,0 @@
{
"@@locale": "fr",
"appTitle": "TetraQ",
"welcomeTitle": "BIENVENUE DANS TETRAQ !",
"nameHint": "NOM",
"saveAndPlay": "SAUVEGARDER ET JOUER",
"onlineTitle": "EN LIGNE",
"onlineSub": "Défiez le monde",
"cpuTitle": "VS CPU",
"cpuSub": "Entraînez avec l'IA",
"localTitle": "LOCAL",
"localSub": "Même écran",
"leaderboardTitle": "CLASSEMENT",
"questsTitle": "QUÊTES",
"themesTitle": "THÈMES",
"tutorialTitle": "TUTORIEL",
"startGame": "JOUER",
"createMatch": "CRÉER UN MATCH",
"joinMatch": "REJOINDRE",
"gameOver": "FIN DE PARTIE",
"mainMenu": "RETOUR AU MENU",
"exit": "QUITTER"
}

View file

@ -1,36 +1,4 @@
{ {
"@@locale": "it",
"appTitle": "TetraQ", "appTitle": "TetraQ",
"welcomeTitle": "BENVENUTO IN TETRAQ!", "playLocal": "PASS & PLAY (Locale)"
"nameHint": "NOME", }
"saveAndPlay": "SALVA E GIOCA",
"onlineTitle": "ONLINE",
"onlineSub": "Sfida il mondo",
"cpuTitle": "VS CPU",
"cpuSub": "Allenati con l'IA",
"localTitle": "LOCALE",
"localSub": "Stesso schermo",
"leaderboardTitle": "CLASSIFICA",
"questsTitle": "SFIDE",
"themesTitle": "TEMI",
"tutorialTitle": "TUTORIAL",
"startGame": "AVVIA PARTITA",
"createMatch": "CREA PARTITA",
"joinMatch": "UNISCITI",
"gameOver": "FINE PARTITA",
"mainMenu": "TORNA AL MENU",
"exit": "ESCI",
"roomSettings": "IMPOSTAZIONI STANZA",
"arenaShape": "FORMA ARENA",
"arenaSize": "GRANDEZZA",
"timeAndOptions": "TEMPO E OPZIONI",
"timeLabel": "TEMPO",
"btnStart": "AVVIA",
"btnCancel": "ANNULLA",
"wordOr": "OPPURE",
"codeHint": "CODICE",
"publicLobbyTitle": "LOBBY PUBBLICA",
"emptyLobbyMsg": "Nessuna stanza pubblica al momento.\nCreane una tu!",
"roomOf": "Stanza di",
"btnEnter": "ENTRA"
}

View file

@ -1,364 +0,0 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
import 'app_localizations_es.dart';
import 'app_localizations_fr.dart';
import 'app_localizations_it.dart';
import 'app_localizations_pt.dart';
import 'app_localizations_ru.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('de'),
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('it'),
Locale('pt'),
Locale('ru'),
Locale('zh'),
];
/// No description provided for @appTitle.
///
/// In it, this message translates to:
/// **'TetraQ'**
String get appTitle;
/// No description provided for @welcomeTitle.
///
/// In it, this message translates to:
/// **'BENVENUTO IN TETRAQ!'**
String get welcomeTitle;
/// No description provided for @nameHint.
///
/// In it, this message translates to:
/// **'NOME'**
String get nameHint;
/// No description provided for @saveAndPlay.
///
/// In it, this message translates to:
/// **'SALVA E GIOCA'**
String get saveAndPlay;
/// No description provided for @onlineTitle.
///
/// In it, this message translates to:
/// **'ONLINE'**
String get onlineTitle;
/// No description provided for @onlineSub.
///
/// In it, this message translates to:
/// **'Sfida il mondo'**
String get onlineSub;
/// No description provided for @cpuTitle.
///
/// In it, this message translates to:
/// **'VS CPU'**
String get cpuTitle;
/// No description provided for @cpuSub.
///
/// In it, this message translates to:
/// **'Allenati con l\'IA'**
String get cpuSub;
/// No description provided for @localTitle.
///
/// In it, this message translates to:
/// **'LOCALE'**
String get localTitle;
/// No description provided for @localSub.
///
/// In it, this message translates to:
/// **'Stesso schermo'**
String get localSub;
/// No description provided for @leaderboardTitle.
///
/// In it, this message translates to:
/// **'CLASSIFICA'**
String get leaderboardTitle;
/// No description provided for @questsTitle.
///
/// In it, this message translates to:
/// **'SFIDE'**
String get questsTitle;
/// No description provided for @themesTitle.
///
/// In it, this message translates to:
/// **'TEMI'**
String get themesTitle;
/// No description provided for @tutorialTitle.
///
/// In it, this message translates to:
/// **'TUTORIAL'**
String get tutorialTitle;
/// No description provided for @startGame.
///
/// In it, this message translates to:
/// **'AVVIA PARTITA'**
String get startGame;
/// No description provided for @createMatch.
///
/// In it, this message translates to:
/// **'CREA PARTITA'**
String get createMatch;
/// No description provided for @joinMatch.
///
/// In it, this message translates to:
/// **'UNISCITI'**
String get joinMatch;
/// No description provided for @gameOver.
///
/// In it, this message translates to:
/// **'FINE PARTITA'**
String get gameOver;
/// No description provided for @mainMenu.
///
/// In it, this message translates to:
/// **'TORNA AL MENU'**
String get mainMenu;
/// No description provided for @exit.
///
/// In it, this message translates to:
/// **'ESCI'**
String get exit;
/// No description provided for @roomSettings.
///
/// In it, this message translates to:
/// **'IMPOSTAZIONI STANZA'**
String get roomSettings;
/// No description provided for @arenaShape.
///
/// In it, this message translates to:
/// **'FORMA ARENA'**
String get arenaShape;
/// No description provided for @arenaSize.
///
/// In it, this message translates to:
/// **'GRANDEZZA'**
String get arenaSize;
/// No description provided for @timeAndOptions.
///
/// In it, this message translates to:
/// **'TEMPO E OPZIONI'**
String get timeAndOptions;
/// No description provided for @timeLabel.
///
/// In it, this message translates to:
/// **'TEMPO'**
String get timeLabel;
/// No description provided for @btnStart.
///
/// In it, this message translates to:
/// **'AVVIA'**
String get btnStart;
/// No description provided for @btnCancel.
///
/// In it, this message translates to:
/// **'ANNULLA'**
String get btnCancel;
/// No description provided for @wordOr.
///
/// In it, this message translates to:
/// **'OPPURE'**
String get wordOr;
/// No description provided for @codeHint.
///
/// In it, this message translates to:
/// **'CODICE'**
String get codeHint;
/// No description provided for @publicLobbyTitle.
///
/// In it, this message translates to:
/// **'LOBBY PUBBLICA'**
String get publicLobbyTitle;
/// No description provided for @emptyLobbyMsg.
///
/// In it, this message translates to:
/// **'Nessuna stanza pubblica al momento.\nCreane una tu!'**
String get emptyLobbyMsg;
/// No description provided for @roomOf.
///
/// In it, this message translates to:
/// **'Stanza di'**
String get roomOf;
/// No description provided for @btnEnter.
///
/// In it, this message translates to:
/// **'ENTRA'**
String get btnEnter;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) => <String>[
'de',
'en',
'es',
'fr',
'it',
'pt',
'ru',
'zh',
].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'de':
return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
case 'es':
return AppLocalizationsEs();
case 'fr':
return AppLocalizationsFr();
case 'it':
return AppLocalizationsIt();
case 'pt':
return AppLocalizationsPt();
case 'ru':
return AppLocalizationsRu();
case 'zh':
return AppLocalizationsZh();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}

View file

@ -1,110 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for German (`de`).
class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'WILLKOMMEN BEI TETRAQ!';
@override
String get nameHint => 'NAME';
@override
String get saveAndPlay => 'SPEICHERN & SPIELEN';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Fordere die Welt heraus';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Trainiere mit KI';
@override
String get localTitle => 'LOKAL';
@override
String get localSub => 'Gleicher Bildschirm';
@override
String get leaderboardTitle => 'RANGLISTE';
@override
String get questsTitle => 'MISSIONEN';
@override
String get themesTitle => 'THEMEN';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'SPIEL STARTEN';
@override
String get createMatch => 'SPIEL ERSTELLEN';
@override
String get joinMatch => 'BEITRETEN';
@override
String get gameOver => 'SPIELENDE';
@override
String get mainMenu => 'ZURÜCK ZUM MENÜ';
@override
String get exit => 'BEENDEN';
@override
String get roomSettings => 'IMPOSTAZIONI STANZA';
@override
String get arenaShape => 'FORMA ARENA';
@override
String get arenaSize => 'GRANDEZZA';
@override
String get timeAndOptions => 'TEMPO E OPZIONI';
@override
String get timeLabel => 'TEMPO';
@override
String get btnStart => 'AVVIA';
@override
String get btnCancel => 'ANNULLA';
@override
String get wordOr => 'OPPURE';
@override
String get codeHint => 'CODICE';
@override
String get publicLobbyTitle => 'LOBBY PUBBLICA';
@override
String get emptyLobbyMsg =>
'Nessuna stanza pubblica al momento.\nCreane una tu!';
@override
String get roomOf => 'Stanza di';
@override
String get btnEnter => 'ENTRA';
}

View file

@ -1,109 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'WELCOME TO TETRAQ!';
@override
String get nameHint => 'NAME';
@override
String get saveAndPlay => 'SAVE & PLAY';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Challenge the world';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Train with AI';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Same screen';
@override
String get leaderboardTitle => 'LEADERBOARD';
@override
String get questsTitle => 'QUESTS';
@override
String get themesTitle => 'THEMES';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'START GAME';
@override
String get createMatch => 'CREATE MATCH';
@override
String get joinMatch => 'JOIN';
@override
String get gameOver => 'GAME OVER';
@override
String get mainMenu => 'BACK TO MENU';
@override
String get exit => 'EXIT';
@override
String get roomSettings => 'ROOM SETTINGS';
@override
String get arenaShape => 'ARENA SHAPE';
@override
String get arenaSize => 'SIZE';
@override
String get timeAndOptions => 'TIME & OPTIONS';
@override
String get timeLabel => 'TIME';
@override
String get btnStart => 'START';
@override
String get btnCancel => 'CANCEL';
@override
String get wordOr => 'OR';
@override
String get codeHint => 'CODE';
@override
String get publicLobbyTitle => 'PUBLIC LOBBY';
@override
String get emptyLobbyMsg => 'No public rooms right now.\nCreate one!';
@override
String get roomOf => 'Room of';
@override
String get btnEnter => 'ENTER';
}

View file

@ -1,110 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Spanish Castilian (`es`).
class AppLocalizationsEs extends AppLocalizations {
AppLocalizationsEs([String locale = 'es']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => '¡BIENVENIDO A TETRAQ!';
@override
String get nameHint => 'NOMBRE';
@override
String get saveAndPlay => 'GUARDAR Y JUGAR';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Desafía al mundo';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Entrena con IA';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Misma pantalla';
@override
String get leaderboardTitle => 'RANKING';
@override
String get questsTitle => 'MISIONES';
@override
String get themesTitle => 'TEMAS';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'INICIAR JUEGO';
@override
String get createMatch => 'CREAR PARTIDA';
@override
String get joinMatch => 'UNIRSE';
@override
String get gameOver => 'FIN DEL JUEGO';
@override
String get mainMenu => 'VOLVER AL MENÚ';
@override
String get exit => 'SALIR';
@override
String get roomSettings => 'IMPOSTAZIONI STANZA';
@override
String get arenaShape => 'FORMA ARENA';
@override
String get arenaSize => 'GRANDEZZA';
@override
String get timeAndOptions => 'TEMPO E OPZIONI';
@override
String get timeLabel => 'TEMPO';
@override
String get btnStart => 'AVVIA';
@override
String get btnCancel => 'ANNULLA';
@override
String get wordOr => 'OPPURE';
@override
String get codeHint => 'CODICE';
@override
String get publicLobbyTitle => 'LOBBY PUBBLICA';
@override
String get emptyLobbyMsg =>
'Nessuna stanza pubblica al momento.\nCreane una tu!';
@override
String get roomOf => 'Stanza di';
@override
String get btnEnter => 'ENTRA';
}

View file

@ -1,110 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for French (`fr`).
class AppLocalizationsFr extends AppLocalizations {
AppLocalizationsFr([String locale = 'fr']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'BIENVENUE DANS TETRAQ !';
@override
String get nameHint => 'NOM';
@override
String get saveAndPlay => 'SAUVEGARDER ET JOUER';
@override
String get onlineTitle => 'EN LIGNE';
@override
String get onlineSub => 'Défiez le monde';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Entraînez avec l\'IA';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Même écran';
@override
String get leaderboardTitle => 'CLASSEMENT';
@override
String get questsTitle => 'QUÊTES';
@override
String get themesTitle => 'THÈMES';
@override
String get tutorialTitle => 'TUTORIEL';
@override
String get startGame => 'JOUER';
@override
String get createMatch => 'CRÉER UN MATCH';
@override
String get joinMatch => 'REJOINDRE';
@override
String get gameOver => 'FIN DE PARTIE';
@override
String get mainMenu => 'RETOUR AU MENU';
@override
String get exit => 'QUITTER';
@override
String get roomSettings => 'IMPOSTAZIONI STANZA';
@override
String get arenaShape => 'FORMA ARENA';
@override
String get arenaSize => 'GRANDEZZA';
@override
String get timeAndOptions => 'TEMPO E OPZIONI';
@override
String get timeLabel => 'TEMPO';
@override
String get btnStart => 'AVVIA';
@override
String get btnCancel => 'ANNULLA';
@override
String get wordOr => 'OPPURE';
@override
String get codeHint => 'CODICE';
@override
String get publicLobbyTitle => 'LOBBY PUBBLICA';
@override
String get emptyLobbyMsg =>
'Nessuna stanza pubblica al momento.\nCreane una tu!';
@override
String get roomOf => 'Stanza di';
@override
String get btnEnter => 'ENTRA';
}

View file

@ -1,110 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Italian (`it`).
class AppLocalizationsIt extends AppLocalizations {
AppLocalizationsIt([String locale = 'it']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'BENVENUTO IN TETRAQ!';
@override
String get nameHint => 'NOME';
@override
String get saveAndPlay => 'SALVA E GIOCA';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Sfida il mondo';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Allenati con l\'IA';
@override
String get localTitle => 'LOCALE';
@override
String get localSub => 'Stesso schermo';
@override
String get leaderboardTitle => 'CLASSIFICA';
@override
String get questsTitle => 'SFIDE';
@override
String get themesTitle => 'TEMI';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'AVVIA PARTITA';
@override
String get createMatch => 'CREA PARTITA';
@override
String get joinMatch => 'UNISCITI';
@override
String get gameOver => 'FINE PARTITA';
@override
String get mainMenu => 'TORNA AL MENU';
@override
String get exit => 'ESCI';
@override
String get roomSettings => 'IMPOSTAZIONI STANZA';
@override
String get arenaShape => 'FORMA ARENA';
@override
String get arenaSize => 'GRANDEZZA';
@override
String get timeAndOptions => 'TEMPO E OPZIONI';
@override
String get timeLabel => 'TEMPO';
@override
String get btnStart => 'AVVIA';
@override
String get btnCancel => 'ANNULLA';
@override
String get wordOr => 'OPPURE';
@override
String get codeHint => 'CODICE';
@override
String get publicLobbyTitle => 'LOBBY PUBBLICA';
@override
String get emptyLobbyMsg =>
'Nessuna stanza pubblica al momento.\nCreane una tu!';
@override
String get roomOf => 'Stanza di';
@override
String get btnEnter => 'ENTRA';
}

View file

@ -1,110 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Portuguese (`pt`).
class AppLocalizationsPt extends AppLocalizations {
AppLocalizationsPt([String locale = 'pt']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'BEM-VINDO AO TETRAQ!';
@override
String get nameHint => 'NOME';
@override
String get saveAndPlay => 'SALVAR E JOGAR';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Desafie o mundo';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Treine com a IA';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Mesma tela';
@override
String get leaderboardTitle => 'CLASSIFICAÇÃO';
@override
String get questsTitle => 'DESAFIOS';
@override
String get themesTitle => 'TEMAS';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'INICIAR JOGO';
@override
String get createMatch => 'CRIAR PARTIDA';
@override
String get joinMatch => 'ENTRAR';
@override
String get gameOver => 'FIM DE JOGO';
@override
String get mainMenu => 'VOLTAR AO MENU';
@override
String get exit => 'SAIR';
@override
String get roomSettings => 'IMPOSTAZIONI STANZA';
@override
String get arenaShape => 'FORMA ARENA';
@override
String get arenaSize => 'GRANDEZZA';
@override
String get timeAndOptions => 'TEMPO E OPZIONI';
@override
String get timeLabel => 'TEMPO';
@override
String get btnStart => 'AVVIA';
@override
String get btnCancel => 'ANNULLA';
@override
String get wordOr => 'OPPURE';
@override
String get codeHint => 'CODICE';
@override
String get publicLobbyTitle => 'LOBBY PUBBLICA';
@override
String get emptyLobbyMsg =>
'Nessuna stanza pubblica al momento.\nCreane una tu!';
@override
String get roomOf => 'Stanza di';
@override
String get btnEnter => 'ENTRA';
}

View file

@ -1,110 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Russian (`ru`).
class AppLocalizationsRu extends AppLocalizations {
AppLocalizationsRu([String locale = 'ru']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'ДОБРО ПОЖАЛОВАТЬ В TETRAQ!';
@override
String get nameHint => 'ИМЯ';
@override
String get saveAndPlay => 'СОХРАНИТЬ И ИГРАТЬ';
@override
String get onlineTitle => 'ОНЛАЙН';
@override
String get onlineSub => 'Брось вызов миру';
@override
String get cpuTitle => 'VS ИИ';
@override
String get cpuSub => 'Тренировка с ИИ';
@override
String get localTitle => 'ЛОКАЛЬНО';
@override
String get localSub => 'Один экран';
@override
String get leaderboardTitle => 'РЕЙТИНГ';
@override
String get questsTitle => 'ЗАДАНИЯ';
@override
String get themesTitle => 'ТЕМЫ';
@override
String get tutorialTitle => 'ОБУЧЕНИЕ';
@override
String get startGame => 'НАЧАТЬ ИГРУ';
@override
String get createMatch => 'СОЗДАТЬ ИГРУ';
@override
String get joinMatch => 'ПРИСОЕДИНИТЬСЯ';
@override
String get gameOver => 'ИГРА ОКОНЧЕНА';
@override
String get mainMenu => 'В ГЛАВНОЕ МЕНЮ';
@override
String get exit => 'ВЫХОД';
@override
String get roomSettings => 'IMPOSTAZIONI STANZA';
@override
String get arenaShape => 'FORMA ARENA';
@override
String get arenaSize => 'GRANDEZZA';
@override
String get timeAndOptions => 'TEMPO E OPZIONI';
@override
String get timeLabel => 'TEMPO';
@override
String get btnStart => 'AVVIA';
@override
String get btnCancel => 'ANNULLA';
@override
String get wordOr => 'OPPURE';
@override
String get codeHint => 'CODICE';
@override
String get publicLobbyTitle => 'LOBBY PUBBLICA';
@override
String get emptyLobbyMsg =>
'Nessuna stanza pubblica al momento.\nCreane una tu!';
@override
String get roomOf => 'Stanza di';
@override
String get btnEnter => 'ENTRA';
}

View file

@ -1,110 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Chinese (`zh`).
class AppLocalizationsZh extends AppLocalizations {
AppLocalizationsZh([String locale = 'zh']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => '欢迎来到 TETRAQ';
@override
String get nameHint => '名字';
@override
String get saveAndPlay => '保存并开始';
@override
String get onlineTitle => '在线匹配';
@override
String get onlineSub => '挑战世界';
@override
String get cpuTitle => '人机对战';
@override
String get cpuSub => '与AI训练';
@override
String get localTitle => '本地游戏';
@override
String get localSub => '同屏对战';
@override
String get leaderboardTitle => '排行榜';
@override
String get questsTitle => '任务';
@override
String get themesTitle => '主题';
@override
String get tutorialTitle => '教程';
@override
String get startGame => '开始游戏';
@override
String get createMatch => '创建比赛';
@override
String get joinMatch => '加入';
@override
String get gameOver => '游戏结束';
@override
String get mainMenu => '返回主菜单';
@override
String get exit => '退出';
@override
String get roomSettings => 'IMPOSTAZIONI STANZA';
@override
String get arenaShape => 'FORMA ARENA';
@override
String get arenaSize => 'GRANDEZZA';
@override
String get timeAndOptions => 'TEMPO E OPZIONI';
@override
String get timeLabel => 'TEMPO';
@override
String get btnStart => 'AVVIA';
@override
String get btnCancel => 'ANNULLA';
@override
String get wordOr => 'OPPURE';
@override
String get codeHint => 'CODICE';
@override
String get publicLobbyTitle => 'LOBBY PUBBLICA';
@override
String get emptyLobbyMsg =>
'Nessuna stanza pubblica al momento.\nCreane una tu!';
@override
String get roomOf => 'Stanza di';
@override
String get btnEnter => 'ENTRA';
}

View file

@ -1,23 +0,0 @@
{
"@@locale": "pt",
"appTitle": "TetraQ",
"welcomeTitle": "BEM-VINDO AO TETRAQ!",
"nameHint": "NOME",
"saveAndPlay": "SALVAR E JOGAR",
"onlineTitle": "ONLINE",
"onlineSub": "Desafie o mundo",
"cpuTitle": "VS CPU",
"cpuSub": "Treine com a IA",
"localTitle": "LOCAL",
"localSub": "Mesma tela",
"leaderboardTitle": "CLASSIFICAÇÃO",
"questsTitle": "DESAFIOS",
"themesTitle": "TEMAS",
"tutorialTitle": "TUTORIAL",
"startGame": "INICIAR JOGO",
"createMatch": "CRIAR PARTIDA",
"joinMatch": "ENTRAR",
"gameOver": "FIM DE JOGO",
"mainMenu": "VOLTAR AO MENU",
"exit": "SAIR"
}

View file

@ -1,23 +0,0 @@
{
"@@locale": "ru",
"appTitle": "TetraQ",
"welcomeTitle": "ДОБРО ПОЖАЛОВАТЬ В TETRAQ!",
"nameHint": "ИМЯ",
"saveAndPlay": "СОХРАНИТЬ И ИГРАТЬ",
"onlineTitle": "ОНЛАЙН",
"onlineSub": "Брось вызов миру",
"cpuTitle": "VS ИИ",
"cpuSub": "Тренировка с ИИ",
"localTitle": "ЛОКАЛЬНО",
"localSub": "Один экран",
"leaderboardTitle": "РЕЙТИНГ",
"questsTitle": "ЗАДАНИЯ",
"themesTitle": "ТЕМЫ",
"tutorialTitle": "ОБУЧЕНИЕ",
"startGame": "НАЧАТЬ ИГРУ",
"createMatch": "СОЗДАТЬ ИГРУ",
"joinMatch": "ПРИСОЕДИНИТЬСЯ",
"gameOver": "ИГРА ОКОНЧЕНА",
"mainMenu": "В ГЛАВНОЕ МЕНЮ",
"exit": "ВЫХОД"
}

View file

@ -1,23 +0,0 @@
{
"@@locale": "zh",
"appTitle": "TetraQ",
"welcomeTitle": "欢迎来到 TETRAQ",
"nameHint": "名字",
"saveAndPlay": "保存并开始",
"onlineTitle": "在线匹配",
"onlineSub": "挑战世界",
"cpuTitle": "人机对战",
"cpuSub": "与AI训练",
"localTitle": "本地游戏",
"localSub": "同屏对战",
"leaderboardTitle": "排行榜",
"questsTitle": "任务",
"themesTitle": "主题",
"tutorialTitle": "教程",
"startGame": "开始游戏",
"createMatch": "创建比赛",
"joinMatch": "加入",
"gameOver": "游戏结束",
"mainMenu": "返回主菜单",
"exit": "退出"
}

View file

@ -9,9 +9,7 @@ class _ClosureResult {
final bool closesSomething; final bool closesSomething;
final int netValue; final int netValue;
final bool causesSwap; final bool causesSwap;
final bool isIceTrap; _ClosureResult(this.closesSomething, this.netValue, this.causesSwap);
_ClosureResult(this.closesSomething, this.netValue, this.causesSwap, this.isIceTrap);
} }
class AIEngine { class AIEngine {
@ -21,7 +19,6 @@ class AIEngine {
if (availableLines.isEmpty) return board.lines.first; if (availableLines.isEmpty) return board.lines.first;
// Più il livello è alto, più l'IA è "intelligente"
double smartChance = 0.50 + ((level - 1) * 0.10); double smartChance = 0.50 + ((level - 1) * 0.10);
if (smartChance > 1.0) smartChance = 1.0; if (smartChance > 1.0) smartChance = 1.0;
@ -30,30 +27,17 @@ class AIEngine {
int myScore = board.currentPlayer == Player.red ? board.scoreRed : board.scoreBlue; int myScore = board.currentPlayer == Player.red ? board.scoreRed : board.scoreBlue;
int oppScore = board.currentPlayer == Player.red ? board.scoreBlue : board.scoreRed; int oppScore = board.currentPlayer == Player.red ? board.scoreBlue : board.scoreRed;
// --- NUOVA LOGICA: GESTIONE INVERSIONE (TACTICAL FEEDING) ---
// Se c'è un numero dispari di caselle Scambio aperte, il gioco è "invertito".
// I punti accumulati andranno in regalo all'avversario!
int swapCount = board.boxes.where((b) => b.type == BoxType.swap && !b.isClosed()).length;
bool isInverted = swapCount % 2 != 0;
List<Line> goodClosingMoves = []; List<Line> goodClosingMoves = [];
List<Line> badClosingMoves = []; List<Line> badClosingMoves = [];
List<Line> iceTraps = [];
for (var line in availableLines) { for (var line in availableLines) {
var result = _checkClosure(board, line, isInverted); var result = _checkClosure(board, line);
if (result.isIceTrap) {
iceTraps.add(line);
continue;
}
if (result.closesSomething) { if (result.closesSomething) {
if (result.causesSwap) { if (result.causesSwap) {
if (myScore < oppScore) { if (myScore < oppScore) {
goodClosingMoves.add(line); // Se perdiamo, lo scambio è la mossa vincente! goodClosingMoves.add(line);
} else { } else {
badClosingMoves.add(line); // Se vinciamo, NON tocchiamo lo scambio! badClosingMoves.add(line);
} }
} else { } else {
if (result.netValue >= 0) { if (result.netValue >= 0) {
@ -75,7 +59,7 @@ class AIEngine {
// --- REGOLA 2: Mosse Sicure --- // --- REGOLA 2: Mosse Sicure ---
List<Line> safeMoves = []; List<Line> safeMoves = [];
for (var line in availableLines) { for (var line in availableLines) {
if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && !iceTraps.contains(line) && _isSafeMove(board, line, myScore, oppScore, isInverted)) { if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && _isSafeMove(board, line, myScore, oppScore)) {
safeMoves.add(line); safeMoves.add(line);
} }
} }
@ -92,14 +76,13 @@ class AIEngine {
// --- REGOLA 3: Scegliere il male minore --- // --- REGOLA 3: Scegliere il male minore ---
if (beSmart) { if (beSmart) {
List<Line> riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l) && !iceTraps.contains(l)).toList(); List<Line> riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l)).toList();
if (riskyButNotTerrible.isNotEmpty) { if (riskyButNotTerrible.isNotEmpty) {
return riskyButNotTerrible[random.nextInt(riskyButNotTerrible.length)]; return riskyButNotTerrible[random.nextInt(riskyButNotTerrible.length)];
} }
} }
// Ultima spiaggia List<Line> nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l)).toList();
List<Line> nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l) && !iceTraps.contains(l)).toList();
if (nonTerribleMoves.isNotEmpty) { if (nonTerribleMoves.isNotEmpty) {
return nonTerribleMoves[random.nextInt(nonTerribleMoves.length)]; return nonTerribleMoves[random.nextInt(nonTerribleMoves.length)];
} }
@ -107,11 +90,10 @@ class AIEngine {
return availableLines[random.nextInt(availableLines.length)]; return availableLines[random.nextInt(availableLines.length)];
} }
static _ClosureResult _checkClosure(GameBoard board, Line line, bool isInverted) { static _ClosureResult _checkClosure(GameBoard board, Line line) {
int netValue = 0; int netValue = 0;
bool closesSomething = false; bool closesSomething = false;
bool causesSwap = false; bool causesSwap = false;
bool isIceTrap = false;
for (var box in board.boxes) { for (var box in board.boxes) {
if (box.type == BoxType.invisible) continue; if (box.type == BoxType.invisible) continue;
@ -124,36 +106,28 @@ class AIEngine {
if (box.right.owner != Player.none || box.right == line) linesCount++; if (box.right.owner != Player.none || box.right == line) linesCount++;
if (linesCount == 4) { if (linesCount == 4) {
if (box.type == BoxType.ice && !line.isIceCracked) { closesSomething = true;
isIceTrap = true;
// FIX: Togliamo la "vista a raggi X" all'Intelligenza Artificiale!
if (box.hiddenJokerOwner == board.currentPlayer) {
// L'IA conosce il suo Jolly, sa che vale +2 e cercherà di chiuderlo
netValue += 2;
} else { } else {
closesSomething = true; // Se c'è il Jolly del giocatore, l'IA NON DEVE SAPERLO e valuta la casella normalmente!
if (box.type == BoxType.gold) netValue += 2;
if (box.type == BoxType.swap) { else if (box.type == BoxType.bomb) netValue -= 1;
causesSwap = true; else if (box.type == BoxType.swap) netValue += 0;
} else { else netValue += 1;
int boxValue = 0;
if (box.hiddenJokerOwner == board.currentPlayer) {
boxValue = 2;
} else {
if (box.type == BoxType.gold) boxValue = 2;
else if (box.type == BoxType.bomb) boxValue = -1;
else if (box.type == BoxType.ice) boxValue = 0;
else if (box.type == BoxType.multiplier) boxValue = 1;
else boxValue = 1;
}
// LA MAGIA: Se il gioco è invertito, fare punti positivi viene calcolato come MALUS per l'IA!
netValue += isInverted ? -boxValue : boxValue;
}
} }
if (box.type == BoxType.swap) causesSwap = true;
} }
} }
} }
return _ClosureResult(closesSomething, netValue, causesSwap, isIceTrap); return _ClosureResult(closesSomething, netValue, causesSwap);
} }
static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore, bool isInverted) { static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore) {
for (var box in board.boxes) { for (var box in board.boxes) {
if (box.type == BoxType.invisible) continue; if (box.type == BoxType.invisible) continue;
@ -165,32 +139,31 @@ class AIEngine {
if (box.right.owner != Player.none) currentLinesCount++; if (box.right.owner != Player.none) currentLinesCount++;
if (currentLinesCount == 2) { if (currentLinesCount == 2) {
int valueForOpponent = 0;
if (box.type == BoxType.ice) { // Nuova logica di sicurezza: cosa succede se l'IA lascia questa scatola all'avversario?
valueForOpponent = -5; int valueForOpponent = 0;
} else if (box.type == BoxType.swap) { if (box.hiddenJokerOwner == board.currentPlayer) {
if (myScore < oppScore) { // Se l'avversario la chiude, becca la trappola dell'IA (-1).
continue; // Sicuro lasciarlo: se lo prende perde i punti. // Quindi PER L'IA È SICURISSIMO LASCIARE QUESTA CASELLA APERTA!
} else {
return false; // Pericoloso: se lo prende ci ruba il vantaggio!
}
} else if (box.hiddenJokerOwner == board.currentPlayer) {
valueForOpponent = -1; valueForOpponent = -1;
} else { } else {
if (box.type == BoxType.gold) valueForOpponent = 2; if (box.type == BoxType.gold) valueForOpponent = 2;
else if (box.type == BoxType.bomb) valueForOpponent = -1; else if (box.type == BoxType.bomb) valueForOpponent = -1;
else if (box.type == BoxType.multiplier) valueForOpponent = 1; else if (box.type == BoxType.swap) valueForOpponent = 0;
else valueForOpponent = 1; else valueForOpponent = 1;
} }
// LA MAGIA 2: Se il tabellone è invertito, regalare un punto all'avversario è un'ottima esca! // Se per l'avversario vale -1 (bomba normale o trappola dell'IA), lasciamogliela!
if (isInverted && box.type != BoxType.swap && box.type != BoxType.ice) { if (valueForOpponent < 0) {
valueForOpponent = -valueForOpponent; continue;
} }
if (valueForOpponent < 0) { if (box.type == BoxType.swap) {
continue; // Mossa considerata sicura (trappola perfetta) if (myScore < oppScore) {
continue;
} else {
return false;
}
} }
return false; return false;

View file

@ -17,12 +17,6 @@ import '../services/storage_service.dart';
import '../services/multiplayer_service.dart'; import '../services/multiplayer_service.dart';
import '../core/app_colors.dart'; import '../core/app_colors.dart';
class CpuMatchSetup {
final int radius;
final ArenaShape shape;
CpuMatchSetup(this.radius, this.shape);
}
class GameController extends ChangeNotifier { class GameController extends ChangeNotifier {
late GameBoard board; late GameBoard board;
bool isVsCPU = false; bool isVsCPU = false;
@ -37,11 +31,9 @@ class GameController extends ChangeNotifier {
bool _hasSavedResult = false; bool _hasSavedResult = false;
Timer? _blitzTimer; Timer? _blitzTimer;
int timeLeft = 10; int timeLeft = 15;
int maxTime = 10; final int maxTime = 15;
String timeModeSetting = 'fixed'; // 'fixed', 'relax', 'dynamic' bool isTimeMode = true;
bool get isTimeMode => timeModeSetting != 'relax';
int consecutiveRematches = 0; // Contatore per la modalità Dinamica
String effectText = ''; String effectText = '';
Color effectColor = Colors.transparent; Color effectColor = Colors.transparent;
@ -58,31 +50,6 @@ class GameController extends ChangeNotifier {
bool opponentWantsRematch = false; bool opponentWantsRematch = false;
int lastMatchXP = 0; int lastMatchXP = 0;
static const Map<int, List<Map<String, dynamic>>> rewardsRoadmap = {
2: [{'title': 'Bomba & Oro', 'desc': 'Appaiono le caselle speciali: Oro (+2) e Bomba (-1)!', 'icon': Icons.stars, 'color': Colors.amber}],
3: [
{'title': 'Tema Cyberpunk', 'desc': 'Sbloccato un nuovo tema visivo nelle impostazioni.', 'icon': Icons.palette, 'color': Colors.tealAccent},
{'title': 'Arena a Croce', 'desc': 'Sbloccata una nuova forma arena più complessa.', 'icon': Icons.add_box, 'color': Colors.blueAccent}
],
5: [{'title': 'Scambio', 'desc': 'Nuova casella! Inverte istantaneamente i punteggi.', 'icon': Icons.swap_horiz, 'color': Colors.purpleAccent}],
7: [
{'title': 'Tema 8-Bit', 'desc': 'Sbloccato il nostalgico tema sala giochi.', 'icon': Icons.videogame_asset, 'color': Colors.greenAccent},
{'title': 'Arene Caos', 'desc': 'Generazione procedurale sbloccata. Nessuna partita sarà uguale!', 'icon': Icons.all_inclusive, 'color': Colors.redAccent}
],
10: [
{'title': 'Tema Grimorio', 'desc': 'Sbloccato il tema della magia antica.', 'icon': Icons.auto_stories, 'color': Colors.deepPurpleAccent},
{'title': 'Blocco di Ghiaccio', 'desc': 'Nuova meccanica! Il ghiaccio richiede due colpi per rompersi.', 'icon': Icons.ac_unit, 'color': Colors.cyanAccent}
],
15: [
{'title': 'Tema Musica', 'desc': 'Sbloccato il tema a tempo di beat.', 'icon': Icons.headphones, 'color': Colors.pinkAccent},
{'title': 'Moltiplicatore x2', 'desc': 'Nuova casella! Raddoppia i punti della tua prossima conquista.', 'icon': Icons.bolt, 'color': Colors.yellowAccent}
],
};
bool hasLeveledUp = false;
int newlyReachedLevel = 1;
List<Map<String, dynamic>> unlockedRewards = [];
bool isSetupPhase = true; bool isSetupPhase = true;
bool myJokerPlaced = false; bool myJokerPlaced = false;
bool oppJokerPlaced = false; bool oppJokerPlaced = false;
@ -94,7 +61,7 @@ class GameController extends ChangeNotifier {
int cpuLevel = 1; int cpuLevel = 1;
int currentMatchLevel = 1; int currentMatchLevel = 1;
int? currentSeed; int? currentSeed;
AppThemeType _activeTheme = AppThemeType.doodle; AppThemeType _activeTheme = AppThemeType.cyberpunk;
String onlineHostName = "ROSSO"; String onlineHostName = "ROSSO";
String onlineGuestName = "BLU"; String onlineGuestName = "BLU";
@ -105,26 +72,7 @@ class GameController extends ChangeNotifier {
startNewGame(radius); startNewGame(radius);
} }
CpuMatchSetup _getSetupForCpuLevel(int level) { void startNewGame(int radius, {bool vsCPU = false, bool isOnline = false, String? roomCode, bool isHost = false, ArenaShape shape = ArenaShape.classic, bool timeMode = true}) {
final rand = Random();
if (level == 1) return CpuMatchSetup(3, ArenaShape.classic);
if (level == 2) return CpuMatchSetup(4, ArenaShape.classic);
if (level == 3) return CpuMatchSetup(4, ArenaShape.cross);
if (level == 4) return CpuMatchSetup(4, ArenaShape.donut);
if (level == 5) return CpuMatchSetup(5, ArenaShape.classic);
if (level == 6) return CpuMatchSetup(4, ArenaShape.hourglass);
if (level == 7) return CpuMatchSetup(5, ArenaShape.cross);
if (level == 8) return CpuMatchSetup(5, ArenaShape.donut);
if (level == 9) return CpuMatchSetup(5, ArenaShape.hourglass);
List<ArenaShape> hardShapes = [ArenaShape.classic, ArenaShape.cross, ArenaShape.donut, ArenaShape.hourglass, ArenaShape.chaos];
ArenaShape chosenShape = hardShapes[rand.nextInt(hardShapes.length)];
int chosenRadius = (chosenShape == ArenaShape.chaos) ? (rand.nextInt(2) + 4) : (rand.nextInt(2) + 5);
return CpuMatchSetup(chosenRadius, chosenShape);
}
void startNewGame(int radius, {bool vsCPU = false, bool isOnline = false, String? roomCode, bool isHost = false, ArenaShape shape = ArenaShape.classic, String timeMode = 'fixed', bool isRematch = false}) {
_onlineSubscription?.cancel(); _onlineSubscription?.cancel();
_onlineSubscription = null; _onlineSubscription = null;
_blitzTimer?.cancel(); _blitzTimer?.cancel();
@ -133,9 +81,6 @@ class GameController extends ChangeNotifier {
_hasSavedResult = false; _hasSavedResult = false;
lastMatchXP = 0; lastMatchXP = 0;
hasLeveledUp = false;
unlockedRewards.clear();
myReaction = null; myReaction = null;
opponentReaction = null; opponentReaction = null;
_lastOpponentReactionTime = null; _lastOpponentReactionTime = null;
@ -151,39 +96,12 @@ class GameController extends ChangeNotifier {
this.isOnline = isOnline; this.isOnline = isOnline;
this.roomCode = roomCode; this.roomCode = roomCode;
this.isHost = isHost; this.isHost = isHost;
this.isTimeMode = timeMode;
if (!isRematch) consecutiveRematches = 0; onlineShape = shape;
this.timeModeSetting = timeMode;
// --- LOGICA TIMER ---
if (this.isVsCPU) {
int pLevel = StorageService.instance.playerLevel;
int calculatedTime = 15 - ((pLevel - 1) * 12 / 14).round();
maxTime = calculatedTime.clamp(3, 15);
} else {
if (timeModeSetting == 'dynamic') {
maxTime = max(2, 10 - (consecutiveRematches * 2));
} else if (timeModeSetting == 'relax') {
maxTime = 0;
} else {
maxTime = 10;
}
}
timeLeft = maxTime;
int finalRadius = radius;
ArenaShape finalShape = shape;
if (this.isVsCPU) {
CpuMatchSetup setup = _getSetupForCpuLevel(cpuLevel);
finalRadius = setup.radius;
finalShape = setup.shape;
}
onlineShape = finalShape;
int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel; int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel;
board = GameBoard(radius: finalRadius, level: levelToUse, seed: currentSeed, shape: finalShape); board = GameBoard(radius: radius, level: levelToUse, seed: currentSeed, shape: onlineShape);
board.currentPlayer = Player.red; board.currentPlayer = Player.red;
isCPUThinking = false; isCPUThinking = false;
@ -196,11 +114,12 @@ class GameController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void placeJoker(int bx, int by) { // --- AGGIUNTA LA COORDINATA Z PER IL 3D ---
void placeJoker(int bx, int by, {int bz = 0}) {
if (!isSetupPhase) return; if (!isSetupPhase) return;
Box? target; Box? target;
try { target = board.boxes.firstWhere((b) => b.x == bx && b.y == by); } catch(e) {} try { target = board.boxes.firstWhere((b) => b.x == bx && b.y == by && b.z == bz); } catch(e) {}
if (target == null || target.type == BoxType.invisible || target.hiddenJokerOwner != null) return; if (target == null || target.type == BoxType.invisible || target.hiddenJokerOwner != null) return;
@ -213,7 +132,7 @@ class GameController extends ChangeNotifier {
String prefix = isHost ? 'p1' : 'p2'; String prefix = isHost ? 'p1' : 'p2';
FirebaseFirestore.instance.collection('games').doc(roomCode).update({ FirebaseFirestore.instance.collection('games').doc(roomCode).update({
'${prefix}_joker': {'x': bx, 'y': by} '${prefix}_joker': {'x': bx, 'y': by, 'z': bz}
}); });
} else { } else {
target.hiddenJokerOwner = jokerTurn; target.hiddenJokerOwner = jokerTurn;
@ -348,9 +267,10 @@ class GameController extends ChangeNotifier {
void _startTimer() { void _startTimer() {
_blitzTimer?.cancel(); _blitzTimer?.cancel();
if (isSetupPhase || !isTimeMode) return; if (isSetupPhase) return;
timeLeft = maxTime; timeLeft = maxTime;
if (!isTimeMode) { notifyListeners(); return; }
_blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (isGameOver || isCPUThinking) { timer.cancel(); return; } if (isGameOver || isCPUThinking) { timer.cancel(); return; }
@ -366,15 +286,16 @@ class GameController extends ChangeNotifier {
void _handleTimeOut() { void _handleTimeOut() {
if (!isTimeMode || isSetupPhase) return; if (!isTimeMode || isSetupPhase) return;
if (isOnline && board.currentPlayer != myPlayer) return; if (isOnline) {
Line randomMove = AIEngine.getBestMove(board, 5);
List<Line> availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList(); handleLineTap(randomMove, _activeTheme, forced: true);
if (availableLines.isEmpty) return; } else if (isVsCPU && board.currentPlayer == Player.red) {
Line randomMove = AIEngine.getBestMove(board, cpuLevel);
final random = Random(); handleLineTap(randomMove, _activeTheme, forced: true);
Line randomMove = availableLines[random.nextInt(availableLines.length)]; } else if (!isVsCPU) {
Line randomMove = AIEngine.getBestMove(board, 5);
handleLineTap(randomMove, _activeTheme, forced: true); handleLineTap(randomMove, _activeTheme, forced: true);
}
} }
void disconnectOnlineGame() { void disconnectOnlineGame() {
@ -403,12 +324,10 @@ class GameController extends ChangeNotifier {
onlineHostName = data['hostName'] ?? "ROSSO"; onlineHostName = data['hostName'] ?? "ROSSO";
onlineGuestName = (data['guestName'] != null && data['guestName'] != '') ? data['guestName'] : "BLU"; onlineGuestName = (data['guestName'] != null && data['guestName'] != '') ? data['guestName'] : "BLU";
// 1. GESTIONE ABBANDONO
if (data['status'] == 'abandoned' && !board.isGameOver && !opponentLeft) { if (data['status'] == 'abandoned' && !board.isGameOver && !opponentLeft) {
opponentLeft = true; notifyListeners(); return; opponentLeft = true; notifyListeners(); return;
} }
// 2. GESTIONE REAZIONI
String? p1React = data['p1_reaction']; String? p1React = data['p1_reaction'];
Timestamp? p1Time = data['p1_reaction_time'] as Timestamp?; Timestamp? p1Time = data['p1_reaction_time'] as Timestamp?;
String? p2React = data['p2_reaction']; String? p2React = data['p2_reaction'];
@ -422,56 +341,47 @@ class GameController extends ChangeNotifier {
_showReaction(false, p1React); _showReaction(false, p1React);
} }
// 3. LOGICA RIVINCITA MIGLIORATA
bool p1Rematch = data['p1_rematch'] ?? false; bool p1Rematch = data['p1_rematch'] ?? false;
bool p2Rematch = data['p2_rematch'] ?? false; bool p2Rematch = data['p2_rematch'] ?? false;
opponentWantsRematch = isHost ? p2Rematch : p1Rematch; opponentWantsRematch = isHost ? p2Rematch : p1Rematch;
// SOLO L'HOST si occupa di chiamare resetMatch sul server if (data['status'] == 'playing' && (data['moves'] as List).isEmpty && rematchRequested) {
if (isHost && p1Rematch && p2Rematch && data['status'] != 'playing') { currentSeed = data['seed'];
startNewGame(data['radius'], isOnline: true, roomCode: roomCode, isHost: isHost, shape: ArenaShape.values.firstWhere((e) => e.name == data['shape']), timeMode: data['timeMode']);
return;
}
if (p1Rematch && p2Rematch && isHost && data['status'] != 'playing') {
currentMatchLevel++; currentMatchLevel++;
int newSeed = DateTime.now().millisecondsSinceEpoch % 1000000; int newSeed = DateTime.now().millisecondsSinceEpoch % 1000000;
final rand = Random(); final rand = Random();
int newRadius = rand.nextInt(4) + 3; int newRadius = rand.nextInt(4) + 3;
ArenaShape newShape = ArenaShape.values[rand.nextInt(ArenaShape.values.length)]; ArenaShape newShape = ArenaShape.values[rand.nextInt(ArenaShape.values.length)];
// Questo cambierà lo status in 'playing' e svuoterà l'array moves.
MultiplayerService().resetMatch(roomCode!, newRadius, newShape.name, newSeed); MultiplayerService().resetMatch(roomCode!, newRadius, newShape.name, newSeed);
return; // L'host aspetterà il prossimo trigger dal server con il nuovo seed.
} }
if (isSetupPhase) {
// --- SINCRONIZZAZIONE JOLLY IN 3D ---
if (!isHost && data['p1_joker'] != null && !oppJokerPlaced) {
int jx = data['p1_joker']['x']; int jy = data['p1_joker']['y']; int jz = data['p1_joker']['z'] ?? 0;
board.boxes.firstWhere((b) => b.x == jx && b.y == jy && b.z == jz).hiddenJokerOwner = Player.red;
oppJokerPlaced = true; _checkSetupComplete();
}
if (isHost && data['p2_joker'] != null && !oppJokerPlaced) {
int jx = data['p2_joker']['x']; int jy = data['p2_joker']['y']; int jz = data['p2_joker']['z'] ?? 0;
board.boxes.firstWhere((b) => b.x == jx && b.y == jy && b.z == jz).hiddenJokerOwner = Player.blue;
oppJokerPlaced = true; _checkSetupComplete();
}
}
List<dynamic> moves = data['moves'] ?? [];
int hostLevel = data['matchLevel'] ?? 1;
int? hostSeed = data['seed']; int? hostSeed = data['seed'];
int hostRadius = data['radius'] ?? board.radius; int hostRadius = data['radius'] ?? board.radius;
String shapeStr = data['shape'] ?? 'classic'; String shapeStr = data['shape'] ?? 'classic';
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
String hostTimeMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax');
// TUTTI (Host e Guest) ripartono SOLO quando vedono il reset effettivo (nuovo seed e status 'playing')
if (rematchRequested && data['status'] == 'playing' && hostSeed != null && hostSeed != currentSeed) {
currentSeed = hostSeed;
consecutiveRematches++;
startNewGame(hostRadius, isOnline: true, roomCode: roomCode, isHost: isHost, shape: hostShape, timeMode: hostTimeMode, isRematch: true);
return;
}
// 4. GESTIONE FASE INIZIALE (JOLLY)
if (isSetupPhase) {
if (!isHost && data['p1_joker'] != null && !oppJokerPlaced) {
int jx = data['p1_joker']['x']; int jy = data['p1_joker']['y'];
board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.red;
oppJokerPlaced = true; _checkSetupComplete();
}
if (isHost && data['p2_joker'] != null && !oppJokerPlaced) {
int jx = data['p2_joker']['x']; int jy = data['p2_joker']['y'];
board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.blue;
oppJokerPlaced = true; _checkSetupComplete();
}
}
// 5. AGGIORNAMENTO LIVELLO / SEED (se non in rivincita)
int hostLevel = data['matchLevel'] ?? 1;
onlineShape = hostShape; onlineShape = hostShape;
timeModeSetting = hostTimeMode; isTimeMode = data['timeMode'] ?? true;
if (!rematchRequested && (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed))) { if (!rematchRequested && (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed))) {
currentMatchLevel = hostLevel; currentSeed = hostSeed; currentMatchLevel = hostLevel; currentSeed = hostSeed;
@ -481,12 +391,9 @@ class GameController extends ChangeNotifier {
isCPUThinking = false; notifyListeners(); return; isCPUThinking = false; notifyListeners(); return;
} }
// 6. GESTIONE MOSSE
List<dynamic> moves = data['moves'] ?? [];
int firebaseMovesCount = moves.length; int firebaseMovesCount = moves.length;
int localMovesCount = board.lines.where((l) => l.owner != Player.none).length; int localMovesCount = board.lines.where((l) => l.owner != Player.none).length;
// Resilienza: se il locale ha mosse e il server no (e non stiamo aspettando una rivincita), pulisci.
if (firebaseMovesCount == 0 && localMovesCount > 0 && !rematchRequested) { if (firebaseMovesCount == 0 && localMovesCount > 0 && !rematchRequested) {
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel; int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape); board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape);
@ -494,7 +401,6 @@ class GameController extends ChangeNotifier {
notifyListeners(); return; notifyListeners(); return;
} }
// Applica mosse remote
if (firebaseMovesCount > localMovesCount) { if (firebaseMovesCount > localMovesCount) {
bool newMovesApplied = false; bool newMovesApplied = false;
@ -549,11 +455,10 @@ class GameController extends ChangeNotifier {
if (!forced) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: false); if (!forced) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: false);
notifyListeners(); // Modificato per non riavviare ciecamente il timer _startTimer(); notifyListeners();
if (isOnline && roomCode != null) { if (isOnline && roomCode != null) {
Map<String, dynamic> moveData = { Map<String, dynamic> moveData = {
'id': DateTime.now().millisecondsSinceEpoch,
'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y, 'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y,
'player': myPlayer == Player.red ? 'red' : 'blue' 'player': myPlayer == Player.red ? 'red' : 'blue'
}; };
@ -567,27 +472,17 @@ class GameController extends ChangeNotifier {
if (board.isGameOver) { if (board.isGameOver) {
_saveMatchResult(); _saveMatchResult();
if (isHost) FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'finished'}); if (isHost) FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'finished'});
} else {
_startTimer(); // Rimesso il timer se si continua a giocare online
} }
} else { } else {
if (board.isGameOver) { if (board.isGameOver) _saveMatchResult();
_saveMatchResult(); else if (isVsCPU && board.currentPlayer == Player.blue) _checkCPUTurn();
} else if (isVsCPU && board.currentPlayer == Player.blue) {
_checkCPUTurn(); // Se tocca alla CPU, la CPU fermerà il timer internamente
} else {
_startTimer(); // Se tocca all'umano, facciamo (ri)partire il timer!
}
} }
} }
} }
void _checkCPUTurn() async { void _checkCPUTurn() async {
if (isVsCPU && board.currentPlayer == Player.blue && !board.isGameOver) { if (isVsCPU && board.currentPlayer == Player.blue && !board.isGameOver) {
isCPUThinking = true; isCPUThinking = true; _blitzTimer?.cancel(); notifyListeners();
_blitzTimer?.cancel(); // La CPU inizia a pensare, congela il timer del giocatore
notifyListeners();
await Future.delayed(const Duration(milliseconds: 600)); await Future.delayed(const Duration(milliseconds: 600));
if (!board.isGameOver) { if (!board.isGameOver) {
@ -602,33 +497,15 @@ class GameController extends ChangeNotifier {
_playEffects(newClosed, newGhosts: newGhosts, isOpponent: true); _playEffects(newClosed, newGhosts: newGhosts, isOpponent: true);
isCPUThinking = false; isCPUThinking = false; _startTimer(); notifyListeners();
notifyListeners();
if (board.isGameOver) { if (board.isGameOver) _saveMatchResult();
_saveMatchResult(); else _checkCPUTurn();
} else if (board.currentPlayer == Player.blue) {
// La CPU ha chiuso un quadrato e ha diritto a un'altra mossa
_checkCPUTurn();
} else {
// Turno passato all'umano: il timer riparte!
_startTimer();
}
} }
} }
} }
List<Map<String, dynamic>> _getUnlocks(int oldLevel, int newLevel) { void _saveMatchResult() {
List<Map<String, dynamic>> unlocks = [];
for(int i = oldLevel + 1; i <= newLevel; i++) {
if (rewardsRoadmap.containsKey(i)) {
unlocks.addAll(rewardsRoadmap[i]!);
}
}
return unlocks;
}
Future<void> _saveMatchResult() async {
if (_hasSavedResult) return; if (_hasSavedResult) return;
_hasSavedResult = true; _hasSavedResult = true;
@ -637,54 +514,40 @@ class GameController extends ChangeNotifier {
String myRealName = StorageService.instance.playerName; String myRealName = StorageService.instance.playerName;
if (myRealName.isEmpty) myRealName = "IO"; if (myRealName.isEmpty) myRealName = "IO";
int oldLevel = StorageService.instance.playerLevel;
if (isOnline) { if (isOnline) {
bool isWin = isHost ? board.scoreRed > board.scoreBlue : board.scoreBlue > board.scoreRed; bool isWin = isHost ? board.scoreRed > board.scoreBlue : board.scoreBlue > board.scoreRed;
calculatedXP = isWin ? 20 : (isDraw ? 5 : 2); calculatedXP = isWin ? 20 : (isDraw ? 5 : 2);
String oppName = isHost ? onlineGuestName : onlineHostName; String oppName = isHost ? onlineGuestName : onlineHostName;
int myScore = isHost ? board.scoreRed : board.scoreBlue; int myScore = isHost ? board.scoreRed : board.scoreBlue;
int oppScore = isHost ? board.scoreBlue : board.scoreRed; int oppScore = isHost ? board.scoreBlue : board.scoreRed;
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true); StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
if (isWin) StorageService.instance.updateQuestProgress(0, 1);
if (isWin) await StorageService.instance.updateQuestProgress(0, 1);
} else if (isVsCPU) { } else if (isVsCPU) {
int myScore = board.scoreRed; int cpuScore = board.scoreBlue; int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
bool isWin = myScore > cpuScore; bool isWin = myScore > cpuScore;
calculatedXP = isWin ? (10 + (cpuLevel ~/ 2)).clamp(10, 25) : (isDraw ? 5 : 2); calculatedXP = isWin ? (10 + (cpuLevel * 2)) : (isDraw ? 5 : 2);
if (isWin) { if (isWin) {
await StorageService.instance.addWin(); StorageService.instance.addWin();
await StorageService.instance.updateQuestProgress(1, 1); StorageService.instance.updateQuestProgress(1, 1);
} else if (cpuScore > myScore) { } else if (cpuScore > myScore) {
await StorageService.instance.addLoss(); StorageService.instance.addLoss();
} }
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false); StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false);
} else { } else {
calculatedXP = 2; calculatedXP = 2;
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false); StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false);
} }
if (board.shape != ArenaShape.classic) { if (board.shape != ArenaShape.classic) {
await StorageService.instance.updateQuestProgress(2, 1); StorageService.instance.updateQuestProgress(2, 1);
} }
lastMatchXP = calculatedXP; lastMatchXP = calculatedXP; StorageService.instance.addXP(calculatedXP); notifyListeners();
await StorageService.instance.addXP(calculatedXP);
int newLevel = StorageService.instance.playerLevel;
if (newLevel > oldLevel) {
hasLeveledUp = true;
newlyReachedLevel = newLevel;
unlockedRewards = _getUnlocks(oldLevel, newLevel);
}
notifyListeners();
} }
void increaseLevelAndRestart() { void increaseLevelAndRestart() {
cpuLevel++; StorageService.instance.saveCpuLevel(cpuLevel); cpuLevel++; StorageService.instance.saveCpuLevel(cpuLevel);
startNewGame(board.radius, vsCPU: true, shape: board.shape, timeMode: timeModeSetting); startNewGame(board.radius, vsCPU: true, shape: board.shape, timeMode: isTimeMode);
} }
} }

View file

@ -1,49 +1,23 @@
// ===========================================================================
// FILE: lib/main.dart
// ===========================================================================
import 'dart:io' show Platform;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'core/theme_manager.dart'; import 'core/theme_manager.dart';
import 'logic/game_controller.dart'; import 'logic/game_controller.dart';
import 'ui/home/home_screen.dart'; import 'ui/home/home_screen.dart';
import 'services/storage_service.dart'; import 'services/storage_service.dart'; // <-- Importiamo il servizio
import 'services/audio_service.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
import 'package:firebase_app_check/firebase_app_check.dart';
import 'package:upgrader/upgrader.dart';
import 'package:in_app_update/in_app_update.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:tetraq/l10n/app_localizations.dart';
void main() async { void main() async {
// Assicuriamoci che i motori di Flutter siano pronti
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// 1. Accendiamo Firebase! (Questo ti era sfuggito)
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
await FirebaseAppCheck.instance.activate( // 2. Accendiamo la Memoria Locale!
androidProvider: kDebugMode ? AndroidProvider.debug : AndroidProvider.playIntegrity,
appleProvider: kDebugMode ? AppleProvider.debug : AppleProvider.deviceCheck,
);
try {
// --- BUG FIX: Creiamo l'account anonimo SOLO se non c'è una sessione attiva ---
// In questo modo, una volta fatto il login, non verrai più buttato fuori al riavvio!
if (FirebaseAuth.instance.currentUser == null) {
await FirebaseAuth.instance.signInAnonymously();
}
} catch (e) {
debugPrint("Errore Auth: $e");
}
await StorageService.instance.init(); await StorageService.instance.init();
await AudioService.instance.init();
runApp( runApp(
MultiProvider( MultiProvider(
@ -68,65 +42,7 @@ class TetraQApp extends StatelessWidget {
fontFamily: 'Roboto', fontFamily: 'Roboto',
useMaterial3: true, useMaterial3: true,
), ),
home: const HomeScreen(),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: UpdateWrapper(child: HomeScreen()),
); );
} }
}
// ===========================================================================
// WIDGET WRAPPER PER LA GESTIONE DEGLI AGGIORNAMENTI IBRIDI (iOS/Android)
// ===========================================================================
class UpdateWrapper extends StatefulWidget {
final Widget child;
const UpdateWrapper({super.key, required this.child});
@override
State<UpdateWrapper> createState() => _UpdateWrapperState();
}
class _UpdateWrapperState extends State<UpdateWrapper> {
@override
void initState() {
super.initState();
if (!kIsWeb && Platform.isAndroid) {
_checkForAndroidUpdate();
}
}
Future<void> _checkForAndroidUpdate() async {
try {
final info = await InAppUpdate.checkForUpdate();
if (info.updateAvailability == UpdateAvailability.updateAvailable) {
if (info.flexibleUpdateAllowed) {
await InAppUpdate.startFlexibleUpdate();
await InAppUpdate.completeFlexibleUpdate();
}
else if (info.immediateUpdateAllowed) {
await InAppUpdate.performImmediateUpdate();
}
}
} catch (e) {
debugPrint("Errore in_app_update Android: $e");
}
}
@override
Widget build(BuildContext context) {
if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) {
return UpgradeAlert(
dialogStyle: (Platform.isIOS || Platform.isMacOS)
? UpgradeDialogStyle.cupertino
: UpgradeDialogStyle.material,
showIgnore: false,
showLater: true,
upgrader: Upgrader(),
child: widget.child,
);
}
return widget.child;
}
} }

View file

@ -6,17 +6,19 @@ import 'dart:math';
enum Player { red, blue, none } enum Player { red, blue, none }
enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier }
enum ArenaShape { classic, cross, donut, hourglass, chaos } // --- AGGIUNTA LA FORMA 3D ---
enum ArenaShape { classic, cross, donut, hourglass, chaos, test, pyramid3D }
class Dot { class Dot {
final int x; final int x;
final int y; final int y;
Dot(this.x, this.y); final int z; // --- NUOVO: ALTEZZA 3D ---
Dot(this.x, this.y, {this.z = 0});
@override @override
bool operator ==(Object other) => identical(this, other) || other is Dot && runtimeType == other.runtimeType && x == other.x && y == other.y; bool operator ==(Object other) => identical(this, other) || other is Dot && runtimeType == other.runtimeType && x == other.x && y == other.y && z == other.z;
@override @override
int get hashCode => x.hashCode ^ y.hashCode; int get hashCode => x.hashCode ^ y.hashCode ^ z.hashCode;
} }
class Line { class Line {
@ -34,6 +36,7 @@ class Line {
class Box { class Box {
final int x; final int x;
final int y; final int y;
final int z; // --- NUOVO: PIANO DELLA SCATOLA ---
Player owner = Player.none; Player owner = Player.none;
late Line top, bottom, left, right; late Line top, bottom, left, right;
BoxType type = BoxType.normal; BoxType type = BoxType.normal;
@ -42,7 +45,7 @@ class Box {
Player? hiddenJokerOwner; Player? hiddenJokerOwner;
bool isJokerRevealed = false; bool isJokerRevealed = false;
Box(this.x, this.y); Box(this.x, this.y, {this.z = 0});
bool isClosed() { bool isClosed() {
if (type == BoxType.invisible) return false; if (type == BoxType.invisible) return false;
@ -50,13 +53,13 @@ class Box {
} }
int getCalculatedValue(Player closer) { int getCalculatedValue(Player closer) {
if (hiddenJokerOwner != null) { if (hiddenJokerOwner != null) return (closer == hiddenJokerOwner) ? 2 : -1;
return (closer == hiddenJokerOwner) ? 2 : -1;
}
if (type == BoxType.gold) return 2; if (type == BoxType.gold) return 2;
if (type == BoxType.bomb) return -1; if (type == BoxType.bomb) return -1;
if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0;
return 1;
// --- NUOVO: NELLA PIRAMIDE I PIANI ALTI VALGONO DI PIÙ! ---
return 1 + z;
} }
} }
@ -77,9 +80,7 @@ class GameBoard {
int scoreRed = 0; int scoreRed = 0;
int scoreBlue = 0; int scoreBlue = 0;
bool isGameOver = false; bool isGameOver = false;
Line? lastMove; Line? lastMove;
bool redHasMultiplier = false; bool redHasMultiplier = false;
bool blueHasMultiplier = false; bool blueHasMultiplier = false;
@ -88,213 +89,111 @@ class GameBoard {
} }
void _generateBoard() { void _generateBoard() {
final random = seed != null ? Random(seed) : Random(); dots.clear(); lines.clear(); boxes.clear(); lastMove = null;
int chaosAlgorithm = random.nextInt(5);
if (shape == ArenaShape.chaos) { if (shape == ArenaShape.pyramid3D) {
columns = radius * 2 + 1; // --- LOGICA GENERAZIONE PIRAMIDE 3D ---
rows = (radius * 3) + 2; columns = 4; rows = 4; // Base 4x4
} else { int maxZ = 4; // 4 Piani (0, 1, 2, 3)
columns = radius * 2 + 1;
rows = radius * 2 + 1;
}
dots.clear(); for (int z = 0; z < maxZ; z++) {
lines.clear(); int currentLayerSize = columns - z; // Il piano si restringe man mano che sale
boxes.clear(); for (int y = 0; y < currentLayerSize; y++) {
lastMove = null; for (int x = 0; x < currentLayerSize; x++) {
var box = Box(x, y, z: z);
// Più sali, più possibilità ci sono di trovare Oro o Bombe
if (z > 0 && Random().nextDouble() > 0.8) box.type = BoxType.gold;
if (z > 1 && Random().nextDouble() > 0.85) box.type = BoxType.bomb;
boxes.add(box);
for (int y = 0; y < rows; y++) { Dot tl = _getOrAddDot(x, y, z);
for (int x = 0; x < columns; x++) { Dot tr = _getOrAddDot(x + 1, y, z);
var box = Box(x, y); Dot bl = _getOrAddDot(x, y + 1, z);
bool isVisible = true; Dot br = _getOrAddDot(x + 1, y + 1, z);
if (shape != ArenaShape.chaos) { box.top = _getOrAddLine(tl, tr); box.bottom = _getOrAddLine(bl, br);
int dx = (x - radius).abs(); box.left = _getOrAddLine(tl, bl); box.right = _getOrAddLine(tr, br);
int dy = (y - radius).abs();
isVisible = (dx + dy) <= radius;
if (isVisible) {
switch (shape) {
case ArenaShape.cross:
int spessoreBraccio = radius > 3 ? 1 : 0;
if (dx > spessoreBraccio && dy > spessoreBraccio) isVisible = false; break;
case ArenaShape.donut:
int dimensioneBuco = radius > 3 ? 2 : 1;
if ((dx + dy) <= dimensioneBuco) isVisible = false; break;
case ArenaShape.hourglass:
if (dx > dy) isVisible = false;
if (x == radius && y == radius) isVisible = true; break;
default: break;
}
} }
} else {
double percentY = y / rows;
if (chaosAlgorithm == 0) {
isVisible = (x % 2 == 0) && (random.nextDouble() > 0.15);
} else if (chaosAlgorithm == 1) {
double chance = 0.2 + (percentY * 0.7);
isVisible = random.nextDouble() < chance;
} else if (chaosAlgorithm == 2) {
int midY = rows ~/ 2;
int distFromCenterY = (y - midY).abs();
int allowedWidth = (distFromCenterY / midY * radius).ceil() + 1;
int dx = (x - radius).abs();
isVisible = dx <= allowedWidth && random.nextDouble() > 0.1;
} else if (chaosAlgorithm == 3) {
isVisible = (y % 2 == 0) ? (x < columns - 1) : (x > 0);
if (random.nextDouble() > 0.8) isVisible = false;
} else if (chaosAlgorithm == 4) {
isVisible = random.nextDouble() > 0.45;
}
if (x == radius && y == rows ~/ 2) isVisible = true;
} }
if (!isVisible) {
box.type = BoxType.invisible;
} else if (level > 1) {
double chance = random.nextDouble();
if (chance < 0.08) box.type = BoxType.gold;
else if (chance > 0.92) box.type = BoxType.bomb;
else if (level >= 5 && chance > 0.88 && chance <= 0.92) box.type = BoxType.swap;
else if (level >= 10 && chance > 0.83 && chance <= 0.88) box.type = BoxType.ice;
else if (level >= 15 && chance > 0.78 && chance <= 0.83) box.type = BoxType.multiplier;
}
boxes.add(box);
} }
} _updatePyramidPlayability(); // Blocchiamo i piani superiori!
} else {
// (Qui c'è il codice standard 2D che abbiamo lasciato invariato per non rompere nulla)
columns = shape == ArenaShape.chaos ? radius * 2 + 1 : (shape == ArenaShape.test ? 3 : radius * 2 + 1);
rows = shape == ArenaShape.chaos ? (radius * 3) + 2 : (shape == ArenaShape.test ? 3 : radius * 2 + 1);
// ========================================================= for (int y = 0; y < rows; y++) {
// NUOVO BLOCCO: ELIMINAZIONE SCAMBI PARI for (int x = 0; x < columns; x++) {
// ========================================================= var box = Box(x, y);
int swapCount = boxes.where((b) => b.type == BoxType.swap).length; boxes.add(box);
if (swapCount > 0 && swapCount % 2 == 0) { Dot tl = _getOrAddDot(x, y, 0); Dot tr = _getOrAddDot(x + 1, y, 0);
Box lastSwap = boxes.lastWhere((b) => b.type == BoxType.swap); Dot bl = _getOrAddDot(x, y + 1, 0); Dot br = _getOrAddDot(x + 1, y + 1, 0);
lastSwap.type = BoxType.normal; box.top = _getOrAddLine(tl, tr); box.bottom = _getOrAddLine(bl, br);
} box.left = _getOrAddLine(tl, bl); box.right = _getOrAddLine(tr, br);
// ========================================================= box.top.isPlayable = true; box.bottom.isPlayable = true; box.left.isPlayable = true; box.right.isPlayable = true;
}
for (var box in boxes) {
Dot tl = _getOrAddDot(box.x, box.y);
Dot tr = _getOrAddDot(box.x + 1, box.y);
Dot bl = _getOrAddDot(box.x, box.y + 1);
Dot br = _getOrAddDot(box.x + 1, box.y + 1);
box.top = _getOrAddLine(tl, tr);
box.bottom = _getOrAddLine(bl, br);
box.left = _getOrAddLine(tl, bl);
box.right = _getOrAddLine(tr, br);
if (box.type != BoxType.invisible) {
box.top.isPlayable = true; box.bottom.isPlayable = true;
box.left.isPlayable = true; box.right.isPlayable = true;
} }
} }
} }
Dot _getOrAddDot(int x, int y) { // Sblocca i piani superiori solo se il "pavimento" è solido
for (var dot in dots) { if (dot.x == x && dot.y == y) return dot; } void _updatePyramidPlayability() {
var newDot = Dot(x, y); if (shape != ArenaShape.pyramid3D) return;
dots.add(newDot); return newDot; for (var box in boxes) {
if (box.z == 0) {
if (box.owner == Player.none) {
box.top.isPlayable = true; box.bottom.isPlayable = true; box.left.isPlayable = true; box.right.isPlayable = true;
}
} else {
// Cerca i 4 quadrati del piano di sotto che lo sorreggono
bool isSupported = true;
for (int dy = 0; dy <= 1; dy++) {
for (int dx = 0; dx <= 1; dx++) {
var supportBox = boxes.where((b) => b.z == box.z - 1 && b.x == box.x + dx && b.y == box.y + dy).firstOrNull;
if (supportBox == null || !supportBox.isClosed()) isSupported = false;
}
}
if (box.owner == Player.none) {
box.top.isPlayable = isSupported; box.bottom.isPlayable = isSupported;
box.left.isPlayable = isSupported; box.right.isPlayable = isSupported;
}
}
}
}
Dot _getOrAddDot(int x, int y, int z) {
for (var dot in dots) { if (dot.x == x && dot.y == y && dot.z == z) return dot; }
var newDot = Dot(x, y, z: z); dots.add(newDot); return newDot;
} }
Line _getOrAddLine(Dot a, Dot b) { Line _getOrAddLine(Dot a, Dot b) {
for (var line in lines) { if (line.connects(a, b)) return line; } for (var line in lines) { if (line.connects(a, b)) return line; }
var newLine = Line(a, b); var newLine = Line(a, b); lines.add(newLine); return newLine;
lines.add(newLine); return newLine;
} }
bool playMove(Line lineToPlay, {Player? forcedPlayer}) { bool playMove(Line lineToPlay, {Player? forcedPlayer}) {
if (isGameOver) return false; if (isGameOver) return false;
Player playerMakingMove = forcedPlayer ?? currentPlayer; Player playerMakingMove = forcedPlayer ?? currentPlayer;
Line? actualLine; Line? actualLine;
for (var l in lines) { for (var l in lines) { if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; } }
if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; }
}
if (actualLine == null || actualLine.owner != Player.none || !actualLine.isPlayable) return false; if (actualLine == null || actualLine.owner != Player.none || !actualLine.isPlayable) return false;
// --- LOGICA BLOCCO DI GHIACCIO ---
bool closesIce = false;
for (var box in boxes) {
if (box.type == BoxType.ice && box.owner == Player.none) {
int linesCount = 0;
if (box.top.owner != Player.none || box.top == actualLine) linesCount++;
if (box.bottom.owner != Player.none || box.bottom == actualLine) linesCount++;
if (box.left.owner != Player.none || box.left == actualLine) linesCount++;
if (box.right.owner != Player.none || box.right == actualLine) linesCount++;
if (linesCount == 4) closesIce = true;
}
}
if (closesIce && !actualLine.isIceCracked) {
actualLine.isIceCracked = true;
lastMove = actualLine;
if (forcedPlayer == null) currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red;
else currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red;
return true;
}
actualLine.isIceCracked = false;
actualLine.owner = playerMakingMove; actualLine.owner = playerMakingMove;
lastMove = actualLine; lastMove = actualLine;
bool scoredPoint = false; bool scoredPoint = false;
bool triggeredSwap = false;
for (var box in boxes) { for (var box in boxes) {
if (box.owner == Player.none && box.isClosed()) { if (box.owner == Player.none && box.isClosed()) {
box.owner = playerMakingMove; box.owner = playerMakingMove; scoredPoint = true;
scoredPoint = true;
if (box.hiddenJokerOwner != null) box.isJokerRevealed = true;
int points = box.getCalculatedValue(playerMakingMove); int points = box.getCalculatedValue(playerMakingMove);
if (playerMakingMove == Player.red) scoreRed += points; else scoreBlue += points;
// --- LOGICA MOLTIPLICATORE x2 ---
if (box.type == BoxType.multiplier) {
if (playerMakingMove == Player.red) redHasMultiplier = true;
else blueHasMultiplier = true;
} else if (points != 0) {
if (playerMakingMove == Player.red && redHasMultiplier) {
points *= 2;
redHasMultiplier = false;
} else if (playerMakingMove == Player.blue && blueHasMultiplier) {
points *= 2;
blueHasMultiplier = false;
}
}
if (playerMakingMove == Player.red) { scoreRed += points; }
else { scoreBlue += points; }
if (box.type == BoxType.swap && box.hiddenJokerOwner == null) {
triggeredSwap = true;
}
}
if (box.type == BoxType.invisible && !box.isRevealed) {
if (box.top.owner != Player.none && box.bottom.owner != Player.none &&
box.left.owner != Player.none && box.right.owner != Player.none) {
box.isRevealed = true;
}
} }
} }
if (triggeredSwap) { if (shape == ArenaShape.pyramid3D) _updatePyramidPlayability(); // Ricalcola chi può giocare sopra!
int temp = scoreRed; scoreRed = scoreBlue; scoreBlue = temp;
}
if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) { isGameOver = true; } if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) isGameOver = true;
if (!scoredPoint && !isGameOver) currentPlayer = (playerMakingMove == Player.red) ? Player.blue : Player.red;
if (forcedPlayer == null) {
if (!scoredPoint && !isGameOver) { currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; }
else if (scoredPoint && !isGameOver) { currentPlayer = playerMakingMove; }
} else {
if (!scoredPoint && !isGameOver) { currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; }
else { currentPlayer = forcedPlayer; }
}
return true; return true;
} }

View file

@ -5,141 +5,60 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import '../core/app_colors.dart'; import '../core/app_colors.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AudioService extends ChangeNotifier { class AudioService extends ChangeNotifier {
static final AudioService instance = AudioService._internal(); static final AudioService instance = AudioService._internal();
AudioService._internal(); AudioService._internal();
bool isMuted = false; bool isMuted = false;
// Abbiamo rimosso _sfxPlayer perché ora ogni suono crea un player usa e getta final AudioPlayer _sfxPlayer = AudioPlayer();
final AudioPlayer _bgmPlayer = AudioPlayer();
AppThemeType _currentTheme = AppThemeType.doodle; void toggleMute() {
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
isMuted = prefs.getBool('isMuted') ?? false;
await _bgmPlayer.setReleaseMode(ReleaseMode.loop);
}
void toggleMute() async {
isMuted = !isMuted; isMuted = !isMuted;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isMuted', isMuted);
if (isMuted) {
await _bgmPlayer.pause();
} else {
playBgm(_currentTheme);
}
notifyListeners(); notifyListeners();
} }
Future<void> playBgm(AppThemeType theme) async {
_currentTheme = theme;
await _bgmPlayer.stop();
if (isMuted) return;
String audioPath = '';
switch (theme) {
case AppThemeType.cyberpunk:
audioPath = 'audio/bgm/Cyber_Dystopia.mp3';
break;
case AppThemeType.doodle:
audioPath = 'audio/bgm/Quad_Dreams.mp3';
break;
case AppThemeType.arcade:
audioPath = 'audio/bgm/8-bit_Prowler.mp3';
break;
case AppThemeType.grimorio:
audioPath = 'audio/bgm/Grimorio_Astral.mp3';
break;
case AppThemeType.music:
audioPath = 'audio/bgm/Music_Loop.mp3';
break;
}
if (audioPath.isNotEmpty) {
try {
await _bgmPlayer.play(AssetSource(audioPath), volume: 0.15);
} catch (e) {
debugPrint("Errore riproduzione BGM: $e");
}
}
}
Future<void> stopBgm() async {
await _bgmPlayer.stop();
}
void playLineSfx(AppThemeType theme) async { void playLineSfx(AppThemeType theme) async {
if (isMuted) return; if (isMuted) return;
String file = ''; String file = '';
switch (theme) { switch (theme) {
case AppThemeType.arcade: case AppThemeType.minimal:
case AppThemeType.music: case AppThemeType.arcade: // Suono secco per l'arcade
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: // Suono etereo per la magia
file = 'cyber_line.wav'; break; file = 'cyber_line.wav'; break;
} }
await _sfxPlayer.play(AssetSource('audio/sfx/$file'));
if (file.isNotEmpty) {
try {
final player = AudioPlayer(); // Player dedicato
await player.play(AssetSource('audio/sfx/$file'), volume: 1.0);
player.onPlayerComplete.listen((_) => player.dispose());
} catch (e) {
debugPrint("Errore SFX Linea: $e");
}
}
} }
void playBoxSfx(AppThemeType theme) async { void playBoxSfx(AppThemeType theme) async {
if (isMuted) return; if (isMuted) return;
String file = ''; String file = '';
switch (theme) { switch (theme) {
case AppThemeType.minimal:
case AppThemeType.arcade: case AppThemeType.arcade:
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:
file = 'cyber_box.wav'; break; file = 'cyber_box.wav'; break;
} }
await _sfxPlayer.play(AssetSource('audio/sfx/$file'));
if (file.isNotEmpty) {
try {
final player = AudioPlayer(); // Player dedicato
await player.play(AssetSource('audio/sfx/$file'), volume: 1.0);
player.onPlayerComplete.listen((_) => player.dispose());
} catch (e) {
debugPrint("Errore SFX Box: $e");
}
}
} }
void playBonusSfx() async { void playBonusSfx() async {
if (isMuted) return; if (isMuted) return;
try { await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'));
final player = AudioPlayer(); // Player dedicato
await player.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0);
player.onPlayerComplete.listen((_) => player.dispose());
} catch(e) {}
} }
void playBombSfx() async { void playBombSfx() async {
if (isMuted) return; if (isMuted) return;
try { await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'));
final player = AudioPlayer(); // Player dedicato
await player.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0);
player.onPlayerComplete.listen((_) => player.dispose());
} catch(e) {}
} }
} }

View file

@ -4,19 +4,15 @@
import 'dart:math'; import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
class MultiplayerService { class MultiplayerService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
CollectionReference get _gamesCollection => _firestore.collection('games'); CollectionReference get _gamesCollection => _firestore.collection('games');
CollectionReference get _invitesCollection => _firestore.collection('invites');
// --- MODIFICA QUI: bool isTimeMode è diventato String timeMode --- Future<String> createGameRoom(int boardRadius, String hostName, String shapeName, bool isTimeMode) async {
Future<String> createGameRoom(int boardRadius, String hostName, String shapeName, String timeMode, {bool isPublic = true}) async {
String roomCode = _generateRoomCode(); String roomCode = _generateRoomCode();
int randomSeed = Random().nextInt(1000000); int randomSeed = Random().nextInt(1000000);
@ -29,11 +25,10 @@ class MultiplayerService {
'moves': [], 'moves': [],
'seed': randomSeed, 'seed': randomSeed,
'hostName': hostName, 'hostName': hostName,
'hostUid': _auth.currentUser?.uid,
'guestName': '', 'guestName': '',
'shape': shapeName, 'shape': shapeName,
'timeMode': timeMode, // Salva la stringa ('fixed', 'relax' o 'dynamic') 'timeMode': isTimeMode,
'isPublic': isPublic, // Nuovi campi per Emojis e Rivincita
'p1_reaction': null, 'p1_reaction': null,
'p2_reaction': null, 'p2_reaction': null,
'p1_rematch': false, 'p1_rematch': false,
@ -57,25 +52,8 @@ class MultiplayerService {
return null; return null;
} }
Stream<QuerySnapshot> getPublicRooms() {
return _gamesCollection
.where('status', isEqualTo: 'waiting')
.where('isPublic', isEqualTo: true)
.snapshots();
}
void shareInviteLink(String roomCode) { void shareInviteLink(String roomCode) {
// ECCO IL TUO SMART LINK FIREBASE! String message = "Ehi! Giochiamo a TetraQ? 🎮\nCopia questo intero messaggio e apri l'app per entrare direttamente, oppure inserisci manualmente il codice: $roomCode";
String smartLink = "https://tetraq-32a4a.web.app";
String message = "Ehi! Giochiamo a TetraQ? 🎮\n\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"
"Non hai ancora il gioco? Scaricalo da qui:\n"
"$smartLink";
Share.share(message); Share.share(message);
} }
@ -91,6 +69,7 @@ class MultiplayerService {
)); ));
} }
// --- NUOVI METODI PER REAZIONI E RIVINCITA ---
Future<void> sendReaction(String roomCode, bool isHost, String reaction) async { Future<void> sendReaction(String roomCode, bool isHost, String reaction) async {
try { try {
String prefix = isHost ? 'p1' : 'p2'; String prefix = isHost ? 'p1' : 'p2';
@ -131,29 +110,4 @@ class MultiplayerService {
debugPrint("Errore reset partita: $e"); debugPrint("Errore reset partita: $e");
} }
} }
Future<void> sendInvite(String targetUid, String roomCode, String hostName) async {
try {
await _invitesCollection.add({
'targetUid': targetUid,
'hostName': hostName,
'roomCode': roomCode,
'timestamp': FieldValue.serverTimestamp(),
});
} catch(e) {
debugPrint("Errore invio invito: $e");
}
}
Stream<QuerySnapshot> listenForInvites(String myUid) {
return _invitesCollection.where('targetUid', isEqualTo: myUid).snapshots();
}
Future<void> deleteInvite(String inviteId) async {
try {
await _invitesCollection.doc(inviteId).delete();
} catch(e) {
debugPrint("Errore cancellazione invito: $e");
}
}
} }

View file

@ -3,74 +3,23 @@
// =========================================================================== // ===========================================================================
import 'dart:convert'; import 'dart:convert';
import 'dart:io' show Platform, HttpClient;
import 'dart:async'; // <--- AGGIUNTO PER IL TIMER DELL'HEARTBEAT
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import '../core/app_colors.dart'; import '../core/app_colors.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';
class StorageService { class StorageService {
static final StorageService instance = StorageService._internal(); static final StorageService instance = StorageService._internal();
StorageService._internal(); StorageService._internal();
late SharedPreferences _prefs; late SharedPreferences _prefs;
int _sessionStart = 0;
Timer? _heartbeatTimer; // <--- IL NOSTRO BATTITO CARDIACO
Future<void> init() async { Future<void> init() async {
_prefs = await SharedPreferences.getInstance(); _prefs = await SharedPreferences.getInstance();
_checkDailyQuests(); _checkDailyQuests(); // All'avvio controlliamo se ci sono nuove sfide
_fetchLocationData();
_sessionStart = DateTime.now().millisecondsSinceEpoch;
} }
// --- NUOVI METODI PER GESTIRE LA PRESENZA --- int get savedThemeIndex => _prefs.getInt('theme') ?? AppThemeType.minimal.index;
void startHeartbeat() { Future<void> saveTheme(AppThemeType theme) async => await _prefs.setInt('theme', theme.index);
_heartbeatTimer?.cancel();
// Esegue il sync leggero ogni 60 secondi
_heartbeatTimer = Timer.periodic(const Duration(seconds: 120), (_) {
syncLeaderboard(isHeartbeat: true);
});
}
void stopHeartbeat() {
_heartbeatTimer?.cancel();
}
// ----------------------------------------------
Future<void> _fetchLocationData() async {
if (kIsWeb) return;
try {
final request = await HttpClient().getUrl(Uri.parse('http://ip-api.com/json/'));
final response = await request.close();
final responseBody = await response.transform(utf8.decoder).join();
final data = jsonDecode(responseBody);
await _prefs.setString('last_ip', data['query'] ?? 'Sconosciuto');
await _prefs.setString('last_city', data['city'] ?? 'Sconosciuta');
} catch (e) {
debugPrint("Errore recupero IP: $e");
}
}
String get lastIp => _prefs.getString('last_ip') ?? 'Sconosciuto';
String get lastCity => _prefs.getString('last_city') ?? 'Sconosciuta';
String getTheme() {
final Object? savedTheme = _prefs.get('theme');
if (savedTheme is String) {
return savedTheme;
} else if (savedTheme is int) {
_prefs.remove('theme');
return AppThemeType.doodle.toString();
}
return AppThemeType.doodle.toString();
}
Future<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);
@ -80,43 +29,22 @@ class StorageService {
int get totalXP => _prefs.getInt('totalXP') ?? 0; int get totalXP => _prefs.getInt('totalXP') ?? 0;
// --- SICUREZZA XP: Inviamo solo INCREMENTI al server --- // Modificato per sincronizzare automaticamente la classifica su Firebase
Future<void> addXP(int xp) async { Future<void> addXP(int xp) async {
await _prefs.setInt('totalXP', totalXP + xp); await _prefs.setInt('totalXP', totalXP + xp);
final user = FirebaseAuth.instance.currentUser; syncLeaderboard();
if (user != null) {
await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({
'xp': FieldValue.increment(xp),
'level': playerLevel,
}, SetOptions(merge: true));
}
} }
int get playerLevel => (totalXP / 100).floor() + 1; int get playerLevel => (totalXP / 100).floor() + 1;
int get wins => _prefs.getInt('wins') ?? 0; int get wins => _prefs.getInt('wins') ?? 0;
Future<void> addWin() async { Future<void> addWin() async {
await _prefs.setInt('wins', wins + 1); await _prefs.setInt('wins', wins + 1);
final user = FirebaseAuth.instance.currentUser; syncLeaderboard();
if (user != null) {
await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({
'wins': FieldValue.increment(1),
}, SetOptions(merge: true));
}
} }
int get losses => _prefs.getInt('losses') ?? 0; int get losses => _prefs.getInt('losses') ?? 0;
Future<void> addLoss() async => await _prefs.setInt('losses', losses + 1);
Future<void> addLoss() async {
await _prefs.setInt('losses', losses + 1);
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({
'losses': FieldValue.increment(1),
}, SetOptions(merge: true));
}
}
int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1; int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1;
Future<void> saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level); Future<void> saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level);
@ -124,129 +52,46 @@ class StorageService {
String get playerName => _prefs.getString('playerName') ?? ''; String get playerName => _prefs.getString('playerName') ?? '';
Future<void> savePlayerName(String name) async { Future<void> savePlayerName(String name) async {
await _prefs.setString('playerName', name); await _prefs.setString('playerName', name);
syncLeaderboard(); syncLeaderboard(); // Aggiorna il nome in classifica
} }
// ====================================================================== // --- NUOVO: SINCRONIZZAZIONE CLASSIFICA ONLINE ---
// LOGICA SYNC AGGIORNATA: GESTIONE HEARTBEAT LEGGERO Future<void> syncLeaderboard() async {
// ====================================================================== if (playerName.isNotEmpty) {
Future<void> syncLeaderboard({bool isHeartbeat = false}) async { try {
try { await FirebaseFirestore.instance.collection('leaderboard').doc(playerName).set({
final user = FirebaseAuth.instance.currentUser; 'name': playerName,
if (user == null) return; 'xp': totalXP,
'level': playerLevel,
String name = playerName; 'wins': wins,
if (name.isEmpty) name = "GIOCATORE"; 'lastActive': FieldValue.serverTimestamp(),
}, SetOptions(merge: true));
String targetUid = user.uid; } catch(e) {
// Ignoriamo gli errori se manca la rete, si sincronizzerà dopo
// 1. Calcolo del Playtime effettivo (aggiornato ad ogni sync)
int sessionDurationSec = (DateTime.now().millisecondsSinceEpoch - _sessionStart) ~/ 1000;
int savedPlaytime = _prefs.getInt('total_playtime') ?? 0;
int totalPlaytime = savedPlaytime + sessionDurationSec;
await _prefs.setInt('total_playtime', totalPlaytime);
_sessionStart = DateTime.now().millisecondsSinceEpoch; // Resetta il timer di sessione
// 2. Creazione del payload di base (dati leggeri che cambiano spesso)
Map<String, dynamic> dataToSave = {
'name': name,
'level': playerLevel,
'lastActive': FieldValue.serverTimestamp(),
'playtime': totalPlaytime,
};
// 3. Se NON è un heartbeat, raccogliamo anche i dati "pesanti" (Device info, ecc.)
if (!isHeartbeat) {
String appVer = "N/D";
String devModel = "N/D";
String osName = kIsWeb ? "Web" : Platform.operatingSystem;
try {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
appVer = "${packageInfo.version}+${packageInfo.buildNumber}";
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (!kIsWeb) {
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
devModel = "${androidInfo.brand} ${androidInfo.model}".toUpperCase();
osName = "Android";
} else if (Platform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
devModel = iosInfo.utsname.machine; // Es. "iPhone13,2"
osName = "iOS";
} else if (Platform.isMacOS) {
MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo;
devModel = macInfo.model; // Es. "MacBookPro17,1"
osName = "macOS";
}
}
} catch (e) {
debugPrint("Errore device info: $e");
}
dataToSave['appVersion'] = appVer;
dataToSave['deviceModel'] = devModel;
dataToSave['platform'] = osName;
dataToSave['ip'] = lastIp;
dataToSave['city'] = lastCity;
if (user.metadata.creationTime != null) {
dataToSave['accountCreated'] = Timestamp.fromDate(user.metadata.creationTime!);
}
} }
await FirebaseFirestore.instance.collection('leaderboard').doc(targetUid).set(dataToSave, SetOptions(merge: true));
} catch (e) {
debugPrint("Errore durante la sincronizzazione della classifica: $e");
} }
} }
Future<bool> isUserAdmin() async { // --- NUOVO: GESTIONE SFIDE GIORNALIERE ---
try {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return false;
final doc = await FirebaseFirestore.instance.collection('admins').doc(user.uid).get();
return doc.exists;
} catch (e) {
debugPrint("Errore verifica admin: $e");
return false;
}
}
List<Map<String, String>> get favorites {
List<String> favs = _prefs.getStringList('favorites') ?? [];
return favs.map((e) => Map<String, String>.from(jsonDecode(e))).toList();
}
Future<void> toggleFavorite(String uid, String name) async {
var favs = favorites;
if (favs.any((f) => f['uid'] == uid)) {
favs.removeWhere((f) => f['uid'] == uid);
} else {
favs.add({'uid': uid, 'name': name});
}
await _prefs.setStringList('favorites', favs.map((e) => jsonEncode(e)).toList());
}
bool isFavorite(String uid) {
return favorites.any((f) => f['uid'] == uid);
}
void _checkDailyQuests() { void _checkDailyQuests() {
String today = DateTime.now().toIso8601String().substring(0, 10); String today = DateTime.now().toIso8601String().substring(0, 10);
String lastDate = _prefs.getString('quest_date') ?? ''; String lastDate = _prefs.getString('quest_date') ?? '';
if (today != lastDate) { if (today != lastDate) {
// Nuovo giorno, nuove sfide!
_prefs.setString('quest_date', today); _prefs.setString('quest_date', today);
// Sfida 1: Gioca partite online
_prefs.setInt('q1_type', 0); _prefs.setInt('q1_type', 0);
_prefs.setInt('q1_prog', 0); _prefs.setInt('q1_prog', 0);
_prefs.setInt('q1_target', 3); _prefs.setInt('q1_target', 3);
// Sfida 2: Vinci contro la CPU
_prefs.setInt('q2_type', 1); _prefs.setInt('q2_type', 1);
_prefs.setInt('q2_prog', 0); _prefs.setInt('q2_prog', 0);
_prefs.setInt('q2_target', 2); _prefs.setInt('q2_target', 2);
// Sfida 3: Partite con forme speciali (Croce, Caos, ecc)
_prefs.setInt('q3_type', 2); _prefs.setInt('q3_type', 2);
_prefs.setInt('q3_prog', 0); _prefs.setInt('q3_prog', 0);
_prefs.setInt('q3_target', 2); _prefs.setInt('q3_target', 2);
@ -265,6 +110,7 @@ class StorageService {
} }
} }
// --- STORICO PARTITE ---
List<Map<String, dynamic>> get matchHistory { List<Map<String, dynamic>> get matchHistory {
List<String> history = _prefs.getStringList('matchHistory') ?? []; List<String> history = _prefs.getStringList('matchHistory') ?? [];
return history.map((e) => jsonDecode(e) as Map<String, dynamic>).toList(); return history.map((e) => jsonDecode(e) as Map<String, dynamic>).toList();

BIN
lib/ui/.DS_Store vendored

Binary file not shown.

View file

@ -1,197 +0,0 @@
// ===========================================================================
// FILE: lib/ui/admin/admin_screen.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../../core/theme_manager.dart';
class AdminScreen extends StatelessWidget {
const AdminScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = context.watch<ThemeManager>().currentColors;
return Scaffold(
backgroundColor: theme.background,
appBar: AppBar(
title: Text("DASHBOARD ADMIN 🕵️‍♂️", style: TextStyle(color: theme.text, fontWeight: FontWeight.w900, letterSpacing: 2)),
backgroundColor: theme.background,
iconTheme: IconThemeData(color: theme.text),
elevation: 0,
),
body: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('leaderboard').orderBy('lastActive', descending: true).snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator(color: theme.playerBlue));
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return Center(child: Text("Nessun giocatore trovato nel database.", style: TextStyle(color: theme.text)));
}
final docs = snapshot.data!.docs;
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: docs.length,
itemBuilder: (context, index) {
var data = docs[index].data() as Map<String, dynamic>;
String name = data['name'] ?? 'Fantasma';
int level = data['level'] ?? 1;
int xp = data['xp'] ?? 0;
int wins = data['wins'] ?? 0;
String platform = data['platform'] ?? 'Sconosciuta';
String ip = data['ip'] ?? 'N/D';
String city = data['city'] ?? 'N/D';
String appVersion = data['appVersion'] ?? 'N/D';
String deviceModel = data['deviceModel'] ?? 'N/D';
int playtimeSec = data['playtime'] ?? 0;
int hours = playtimeSec ~/ 3600;
int minutes = (playtimeSec % 3600) ~/ 60;
String playtimeStr = "${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}";
// Recupero della data di creazione dell'account
DateTime? created;
if (data['accountCreated'] != null) created = (data['accountCreated'] as Timestamp).toDate();
DateTime? lastActive;
if (data['lastActive'] != null) lastActive = (data['lastActive'] as Timestamp).toDate();
String createdStr = created != null ? DateFormat('dd MMM yyyy - HH:mm').format(created) : 'N/D';
String lastActiveStr = lastActive != null ? DateFormat('dd MMM yyyy - HH:mm').format(lastActive) : 'N/D';
IconData platformIcon = Icons.device_unknown;
if (platform == 'iOS' || platform == 'macOS') platformIcon = Icons.apple;
if (platform == 'Android') platformIcon = Icons.android;
if (platform == 'Windows') platformIcon = Icons.window;
return Card(
color: theme.text.withOpacity(0.05),
elevation: 0,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
side: BorderSide(color: theme.gridLine.withOpacity(0.3))
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(name, style: TextStyle(color: theme.playerBlue, fontSize: 22, fontWeight: FontWeight.w900)),
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: theme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: theme.playerBlue, width: 2),
),
title: Text("Info Connessione", style: TextStyle(color: theme.text, fontWeight: FontWeight.bold)),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("🌐 IP: $ip", style: TextStyle(color: theme.text, fontSize: 16)),
const SizedBox(height: 10),
Text("📍 Città: $city", style: TextStyle(color: theme.text, fontSize: 16)),
const SizedBox(height: 10),
Text("📱 OS: $platform", style: TextStyle(color: theme.text, fontSize: 16)),
const SizedBox(height: 10),
Text("💻 Hardware: $deviceModel", style: TextStyle(color: theme.text, fontSize: 16)),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text("CHIUDI", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold)),
)
],
),
);
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: theme.text.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(platformIcon, color: theme.text.withOpacity(0.8), size: 24),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Text("Liv. $level", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 14)),
const SizedBox(width: 10),
Text("$xp XP", style: TextStyle(color: theme.text.withOpacity(0.7), fontSize: 12)),
const SizedBox(width: 10),
Text("Vittorie: $wins", style: TextStyle(color: Colors.amber.shade700, fontWeight: FontWeight.bold, fontSize: 12)),
const Spacer(),
Icon(Icons.timer, color: theme.text.withOpacity(0.6), size: 16),
const SizedBox(width: 4),
Text(playtimeStr, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 14)),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Divider(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(fit: BoxFit.scaleDown, child: Text("Registrato il:", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10))),
FittedBox(fit: BoxFit.scaleDown, child: Text(createdStr, style: TextStyle(color: theme.text, fontSize: 12, fontWeight: FontWeight.bold))),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FittedBox(fit: BoxFit.scaleDown, child: Text("Versione App:", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10))),
FittedBox(fit: BoxFit.scaleDown, child: Text("v. $appVersion", style: TextStyle(color: theme.playerBlue, fontSize: 12, fontWeight: FontWeight.bold))),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
FittedBox(fit: BoxFit.scaleDown, child: Text("Ultimo Accesso:", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10))),
FittedBox(fit: BoxFit.scaleDown, child: Text(lastActiveStr, style: TextStyle(color: Colors.green, fontSize: 12, fontWeight: FontWeight.bold))),
],
),
),
],
)
],
),
),
);
},
);
}
),
);
}
}

View file

@ -4,6 +4,7 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '../../models/game_board.dart'; import '../../models/game_board.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
@ -20,6 +21,8 @@ class BoardPainter extends CustomPainter {
final Player myPlayer; final Player myPlayer;
final Player jokerTurn; final Player jokerTurn;
final double cameraAngle; // Angolazione della telecamera a 360 gradi!
BoardPainter({ BoardPainter({
required this.board, required this.board,
required this.theme, required this.theme,
@ -29,348 +32,231 @@ class BoardPainter extends CustomPainter {
required this.isSetupPhase, required this.isSetupPhase,
required this.myPlayer, required this.myPlayer,
required this.jokerTurn, required this.jokerTurn,
this.blinkValue = 0.0 this.blinkValue = 0.0,
this.cameraAngle = 0.0,
}); });
Color _darken(Color c, [double amount = .1]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(c);
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor();
}
// LA MAGIA: Proiezione Isometrica Ruotabile
Offset projectLogical(double x, double y, double z, Size size) {
if (board.shape != ArenaShape.pyramid3D) {
int gridPoints = board.columns + 1;
double spacing = size.width / gridPoints;
return Offset(x * spacing + (spacing / 2), y * spacing + (spacing / 2));
}
double tileW = size.width / 3.8;
double tileH = tileW * 0.55;
double zHeight = tileW * 0.45; // L'altezza fisica del blocco 3D
// Calcoliamo la posizione tenendo conto del restringimento della piramide (+ z * 0.5)
double actualX = x + z * 0.5 - board.columns / 2.0;
double actualY = y + z * 0.5 - board.rows / 2.0;
// Matrice di rotazione della telecamera (Z-Axis)
double rx = actualX * cos(cameraAngle) - actualY * sin(cameraAngle);
double ry = actualX * sin(cameraAngle) + actualY * cos(cameraAngle);
// Proiezione Isometrica 2D
double sx = (rx - ry) * tileW;
// Il SEGRETO: -z sposta fisicamente in ALTO la faccia del cubo rispetto allo schermo
double sy = (rx + ry) * tileH - (z * zHeight);
return Offset(size.width / 2 + sx, size.height * 0.70 + sy);
}
// Calcola la distanza dalla telecamera per lo Z-Buffering
double getDepth(Box b) {
double actualX = b.x + b.z * 0.5 - board.columns / 2.0;
double actualY = b.y + b.z * 0.5 - board.rows / 2.0;
double rx = actualX * cos(cameraAngle) - actualY * sin(cameraAngle);
double ry = actualX * sin(cameraAngle) + actualY * cos(cameraAngle);
return rx + ry; // Ordina dal più lontano al più vicino
}
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
if (themeType == AppThemeType.doodle) { if (board.shape == ArenaShape.pyramid3D) _paint3D(canvas, size);
final Paint paperGridPaint = Paint() else _paint2D(canvas, size);
..color = Colors.grey.withOpacity(0.3) }
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
double paperStep = 20.0; void _paint3D(Canvas canvas, Size size) {
for (double i = 0; i <= size.width; i += paperStep) { double visualZHeight = (size.width / 3.8) * 0.45;
canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint); int maxZ = 4;
} Set<Line> drawnLines = {};
for (double i = 0; i <= size.height; i += paperStep) {
canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
}
}
int gridPoints = board.columns + 1; // Costruiamo la piramide piano per piano, partendo dal basso
double spacing = size.width / gridPoints; for (int currentZ = 0; currentZ < maxZ; currentZ++) {
double offset = spacing / 2; var currentLevelBoxes = board.boxes.where((b) => b.z == currentZ && b.type != BoxType.invisible).toList();
Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset);
// ======================================================================= // Ordiniamo dal fondo allo schermo
// 1. CREAZIONE DELLA SAGOMA DELL'ARENA (SFONDO E BORDO) currentLevelBoxes.sort((a, b) => getDepth(a).compareTo(getDepth(b)));
// =======================================================================
Path arenaShape = Path();
bool isFirst = true;
// Uniamo la forma di ogni box giocabile per creare un'unica sagoma for (var box in currentLevelBoxes) {
for (var box in board.boxes) { bool isPlayable = box.top.isPlayable;
if (box.type != BoxType.invisible) { // Ignora i buchi bool isOwned = box.owner != Player.none;
Offset p1 = getScreenPos(box.x, box.y); if (!isOwned && !isPlayable) continue;
Offset p2 = getScreenPos(box.x + 1, box.y + 1);
Path boxPath = Path()..addRect(Rect.fromPoints(p1, p2));
if (isFirst) { // Disegniamo prima le LINEE DELLA GRIGLIA relative a questo cubo
arenaShape = boxPath; void drawLine(Line l, double lx1, double ly1, double lx2, double ly2) {
isFirst = false; if (drawnLines.contains(l)) return;
} else { drawnLines.add(l);
arenaShape = Path.combine(PathOperation.union, arenaShape, boxPath); if (!l.isPlayable && l.owner == Player.none) return;
Offset pt1 = projectLogical(lx1, ly1, currentZ.toDouble(), size);
Offset pt2 = projectLogical(lx2, ly2, currentZ.toDouble(), size);
if (l.isIceCracked) { _drawCrackedIceLine(canvas, pt1, pt2, blinkValue); return; }
Color lineColor = l.owner == Player.none ? theme.gridLine.withOpacity(0.5) : (l.owner == Player.red ? theme.playerRed : theme.playerBlue);
if (l == board.lastMove && l.owner != Player.none) canvas.drawLine(pt1, pt2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.8)..strokeWidth = 10.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
canvas.drawLine(pt1, pt2, Paint()..color = lineColor..strokeWidth = 4.0..strokeCap = StrokeCap.round);
} }
}
}
// --- DISEGNO DELLO SFONDO LUMINOSO --- drawLine(box.top, box.x.toDouble(), box.y.toDouble(), box.x + 1.0, box.y.toDouble());
final fillPaint = Paint() drawLine(box.right, box.x + 1.0, box.y.toDouble(), box.x + 1.0, box.y + 1.0);
..style = PaintingStyle.fill drawLine(box.bottom, box.x.toDouble(), box.y + 1.0, box.x + 1.0, box.y + 1.0);
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10.0); drawLine(box.left, box.x.toDouble(), box.y.toDouble(), box.x.toDouble(), box.y + 1.0);
if (themeType == AppThemeType.music) { // Disegniamo i PALLINI
fillPaint.color = Colors.white.withOpacity(0.08); void drawDot(Dot d) {
canvas.drawPath(arenaShape, fillPaint); if (board.lines.any((l) => (l.p1 == d || l.p2 == d) && l.isPlayable)) {
} else if (themeType == AppThemeType.cyberpunk) { canvas.drawCircle(projectLogical(d.x.toDouble(), d.y.toDouble(), currentZ.toDouble(), size), 5.0, Paint()..color = theme.text.withOpacity(0.8));
fillPaint.color = theme.playerBlue.withOpacity(0.1);
canvas.drawPath(arenaShape, fillPaint);
}
// --- DISEGNO DEL BORDO ESTERNO SOTTILE ---
double baseStroke = themeType == AppThemeType.grimorio ? 6.0 : 4.0;
if (themeType == AppThemeType.doodle) baseStroke = 2.5;
final outlinePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = baseStroke * 0.5
..strokeJoin = StrokeJoin.round;
if (themeType == AppThemeType.cyberpunk) {
outlinePaint.color = theme.gridLine;
outlinePaint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4.0 * blinkValue.clamp(0.1, 1.0));
}
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.music) { outlinePaint.color = Colors.black; }
else if (themeType == AppThemeType.doodle) { outlinePaint.color = const Color(0xFF111122); }
else { outlinePaint.color = theme.gridLine.withOpacity(0.8); }
// Disegniamo il contorno
canvas.drawPath(arenaShape, outlinePaint);
// =======================================================================
for (var box in board.boxes) {
Offset p1 = getScreenPos(box.x, box.y);
Offset p2 = getScreenPos(box.x + 1, box.y + 1);
Rect rect = Rect.fromPoints(p1, p2);
if (box.type == BoxType.invisible) {
if (box.isRevealed) {
_drawIconInBox(canvas, rect, ThemeIcons.block(themeType), Colors.grey.shade500);
}
continue;
}
// Sfondo azzurrino se è di ghiaccio (anche prima di chiuderla)
if (box.type == BoxType.ice && box.owner == Player.none) {
canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.cyanAccent.withOpacity(0.05)..style=PaintingStyle.fill);
}
if (box.owner != Player.none) {
final boxPaint = Paint()
..style = PaintingStyle.fill
..color = box.owner == Player.red ? theme.playerRed.withOpacity(0.6) : theme.playerBlue.withOpacity(0.6);
if (themeType == AppThemeType.doodle) {
Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700;
_drawScribbleBox(canvas, rect, penColor);
} else if (themeType == AppThemeType.arcade) {
_drawArcadeBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue);
} else if (themeType == AppThemeType.grimorio) {
_drawGrimorioBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue);
} else {
canvas.drawRect(rect, boxPaint);
}
}
if (box.hiddenJokerOwner != null) {
Color jokerColor = box.hiddenJokerOwner == Player.red ? theme.playerRed : theme.playerBlue;
if (box.isJokerRevealed) {
_drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor);
} else {
bool canSee = false;
if (isOnline || isVsCPU) {
canSee = box.hiddenJokerOwner == myPlayer;
} else {
canSee = false;
}
if (canSee) {
_drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor.withOpacity(0.3));
} }
} }
} drawDot(box.top.p1); drawDot(box.top.p2); drawDot(box.bottom.p2); drawDot(box.bottom.p1);
if (box.type == BoxType.gold) { // --- IL CUBO SOLIDO ---
_drawIconInBox(canvas, rect, ThemeIcons.gold(themeType), Colors.amber); if (isOwned) {
} else if (box.type == BoxType.bomb) { // Calcoliamo i 4 vertici del TETTO (Z + 1)
_drawIconInBox(canvas, rect, ThemeIcons.bomb(themeType), themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.greenAccent : Colors.deepPurple); List<Offset> topCorners = [
} else if (box.type == BoxType.swap) { projectLogical(box.x.toDouble(), box.y.toDouble(), currentZ + 1.0, size),
_drawIconInBox(canvas, rect, ThemeIcons.swap(themeType), Colors.purpleAccent); projectLogical(box.x + 1.0, box.y.toDouble(), currentZ + 1.0, size),
} else if (box.type == BoxType.ice) { projectLogical(box.x + 1.0, box.y + 1.0, currentZ + 1.0, size),
_drawIconInBox(canvas, rect, ThemeIcons.ice(themeType), Colors.cyanAccent); projectLogical(box.x.toDouble(), box.y + 1.0, currentZ + 1.0, size),
} else if (box.type == BoxType.multiplier) { ];
_drawIconInBox(canvas, rect, ThemeIcons.multiplier(themeType), Colors.yellowAccent);
}
}
for (var line in board.lines) { // Algoritmo Geometrico Infallibile: troviamo la silhouette
if (!line.isPlayable) continue; topCorners.sort((a, b) => a.dy.compareTo(b.dy));
Offset screenTop = topCorners[0]; // Il punto più alto sullo schermo
Offset screenBottom = topCorners[3]; // Il punto più basso sullo schermo
Offset screenLeft = topCorners[1].dx < topCorners[2].dx ? topCorners[1] : topCorners[2];
Offset screenRight = topCorners[1].dx > topCorners[2].dx ? topCorners[1] : topCorners[2];
Offset p1 = getScreenPos(line.p1.x, line.p1.y); Color baseColor = box.owner == Player.red ? theme.playerRed : theme.playerBlue;
Offset p2 = getScreenPos(line.p2.x, line.p2.y);
// --- DISEGNO DELLA LINEA "INCRINATA" DAL GHIACCIO --- // PARETE SINISTRA: Dal tetto scende verso il basso
if (line.isIceCracked) { Path leftWall = Path()
_drawCrackedIceLine(canvas, p1, p2, blinkValue); ..moveTo(screenLeft.dx, screenLeft.dy)
continue; ..lineTo(screenBottom.dx, screenBottom.dy)
} ..lineTo(screenBottom.dx, screenBottom.dy + visualZHeight)
..lineTo(screenLeft.dx, screenLeft.dy + visualZHeight)
..close();
canvas.drawPath(leftWall, Paint()..color = _darken(baseColor, 0.15)..style = PaintingStyle.fill);
canvas.drawPath(leftWall, Paint()..color = Colors.black.withOpacity(0.3)..style = PaintingStyle.stroke..strokeWidth = 1.0);
bool isLastMove = (line == board.lastMove); // PARETE DESTRA: Dal tetto scende verso il basso
Color lineColor = line.owner == Player.none Path rightWall = Path()
? theme.gridLine.withOpacity(0.4) ..moveTo(screenBottom.dx, screenBottom.dy)
: (line.owner == Player.red ? theme.playerRed : theme.playerBlue); ..lineTo(screenRight.dx, screenRight.dy)
..lineTo(screenRight.dx, screenRight.dy + visualZHeight)
..lineTo(screenBottom.dx, screenBottom.dy + visualZHeight)
..close();
canvas.drawPath(rightWall, Paint()..color = _darken(baseColor, 0.35)..style = PaintingStyle.fill);
canvas.drawPath(rightWall, Paint()..color = Colors.black.withOpacity(0.3)..style = PaintingStyle.stroke..strokeWidth = 1.0);
if (isLastMove && line.owner != Player.none && themeType != AppThemeType.cyberpunk && themeType != AppThemeType.arcade && themeType != AppThemeType.grimorio) { // IL TETTO: Disegnato per ultimo, copre i muri ed è solido
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)); Path roof = Path()..moveTo(screenTop.dx, screenTop.dy)..lineTo(screenRight.dx, screenRight.dy)..lineTo(screenBottom.dx, screenBottom.dy)..lineTo(screenLeft.dx, screenLeft.dy)..close();
} canvas.drawPath(roof, Paint()..color = baseColor..style = PaintingStyle.fill);
canvas.drawPath(roof, Paint()..color = Colors.white.withOpacity(0.5)..style = PaintingStyle.stroke..strokeWidth = 2.0);
if (themeType == AppThemeType.cyberpunk) { _drawBoxIcon(canvas, roof, box);
_drawNeonLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
} else if (themeType == AppThemeType.doodle) {
Color doodleColor = line.owner == Player.none ? Colors.black.withOpacity(0.05) : lineColor;
if (isLastMove && line.owner != Player.none) doodleColor = Color.lerp(doodleColor, Colors.black, blinkValue * 0.4) ?? doodleColor;
_drawWobblyLine(canvas, p1, p2, doodleColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
} else if (themeType == AppThemeType.arcade) {
_drawArcadeLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
} else if (themeType == AppThemeType.grimorio) {
_drawGrimorioLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
} else if (themeType == AppThemeType.music) {
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);
} else {
if (isLastMove && line.owner != Player.none) lineColor = Color.lerp(lineColor, Colors.white, blinkValue * 0.5) ?? lineColor;
canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round);
}
}
final dotPaint = Paint()..style = PaintingStyle.fill; } else if (isPlayable) {
Set<Dot> activeDots = {}; // PAVIMENTO VUOTO
for (var line in board.lines) { Offset f0 = projectLogical(box.x.toDouble(), box.y.toDouble(), currentZ.toDouble(), size);
if (line.isPlayable) { Offset f1 = projectLogical(box.x + 1.0, box.y.toDouble(), currentZ.toDouble(), size);
activeDots.add(line.p1); activeDots.add(line.p2); Offset f2 = projectLogical(box.x + 1.0, box.y + 1.0, currentZ.toDouble(), size);
} Offset f3 = projectLogical(box.x.toDouble(), box.y + 1.0, currentZ.toDouble(), size);
} Path floor = Path()..moveTo(f0.dx, f0.dy)..lineTo(f1.dx, f1.dy)..lineTo(f2.dx, f2.dy)..lineTo(f3.dx, f3.dy)..close();
for (var dot in activeDots) { canvas.drawPath(floor, Paint()..color = Colors.white.withOpacity(0.08)..style = PaintingStyle.fill);
Offset pos = getScreenPos(dot.x, dot.y); _drawBoxIcon(canvas, floor, box);
if (themeType == AppThemeType.cyberpunk) { }
canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3));
canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5));
} else if (themeType == AppThemeType.doodle) {
canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = Colors.black.withOpacity(0.25));
} else if (themeType == AppThemeType.arcade) {
canvas.drawRect(Rect.fromCenter(center: pos, width: 8, height: 8), dotPaint..color = theme.gridLine.withOpacity(0.9));
canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = theme.background);
} else if (themeType == AppThemeType.grimorio) {
canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0));
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));
} else if (themeType == AppThemeType.music) {
canvas.drawCircle(pos, 4.5, dotPaint..color = Colors.black87);
} else {
canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6));
} }
} }
} }
void _drawIconInBox(Canvas canvas, Rect rect, IconData icon, Color color) { void _paint2D(Canvas canvas, Size size) {
TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr); for (var box in board.boxes) {
textPainter.text = TextSpan( if (box.type == BoxType.invisible) continue;
text: String.fromCharCode(icon.codePoint), Offset p1 = projectLogical(box.top.p1.x.toDouble(), box.top.p1.y.toDouble(), 0, size);
style: TextStyle( Offset p2 = projectLogical(box.top.p2.x.toDouble(), box.top.p2.y.toDouble(), 0, size);
color: themeType == AppThemeType.arcade ? color : color.withOpacity(0.7), Offset p3 = projectLogical(box.bottom.p2.x.toDouble(), box.bottom.p2.y.toDouble(), 0, size);
fontSize: rect.width * 0.45, Offset p4 = projectLogical(box.bottom.p1.x.toDouble(), box.bottom.p1.y.toDouble(), 0, size);
fontFamily: icon.fontFamily, Path poly = Path()..moveTo(p1.dx, p1.dy)..lineTo(p2.dx, p2.dy)..lineTo(p3.dx, p3.dy)..lineTo(p4.dx, p4.dy)..close();
package: icon.fontPackage,
shadows: themeType == AppThemeType.arcade ? [] : [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))] if (box.owner != Player.none) {
), Color c = box.owner == Player.red ? theme.playerRed : theme.playerBlue;
); canvas.drawPath(poly, Paint()..color = c.withOpacity(0.85)..style = PaintingStyle.fill);
textPainter.layout(); }
textPainter.paint(canvas, Offset(rect.center.dx - textPainter.width / 2, rect.center.dy - textPainter.height / 2)); _drawBoxIcon(canvas, poly, box);
}
for (var line in board.lines) {
if (!line.isPlayable && line.owner == Player.none) continue;
Offset p1 = projectLogical(line.p1.x.toDouble(), line.p1.y.toDouble(), 0, size);
Offset p2 = projectLogical(line.p2.x.toDouble(), line.p2.y.toDouble(), 0, size);
if (line.isIceCracked) { _drawCrackedIceLine(canvas, p1, p2, blinkValue); continue; }
Color lineColor = line.owner == Player.none ? theme.gridLine.withOpacity(0.4) : (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
if (line == board.lastMove && line.owner != Player.none) canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.8)..strokeWidth = 12.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = (line.owner == Player.none ? 3.0 : 6.0)..strokeCap = StrokeCap.round);
}
for (var dot in board.dots) {
bool isVisible = board.lines.any((l) => (l.p1 == dot || l.p2 == dot) && l.isPlayable);
if (isVisible) canvas.drawCircle(projectLogical(dot.x.toDouble(), dot.y.toDouble(), 0, size), 4.0, Paint()..color = theme.text.withOpacity(0.8));
}
}
void _drawBoxIcon(Canvas canvas, Path face, Box box) {
if (box.type == BoxType.gold) _drawIconOnPath(canvas, face, FontAwesomeIcons.crown, Colors.amber);
else if (box.type == BoxType.bomb) _drawIconOnPath(canvas, face, FontAwesomeIcons.skull, Colors.redAccent);
else if (box.type == BoxType.swap) _drawIconOnPath(canvas, face, FontAwesomeIcons.arrowsRotate, Colors.purpleAccent);
else if (box.type == BoxType.multiplier) _drawIconOnPath(canvas, face, FontAwesomeIcons.bolt, Colors.yellowAccent);
else if (box.type == BoxType.ice && box.owner == Player.none) {
canvas.drawPath(face, Paint()..color = Colors.cyanAccent.withOpacity(0.15)..style = PaintingStyle.fill);
_drawIconOnPath(canvas, face, FontAwesomeIcons.snowflake, Colors.cyanAccent);
}
}
void _drawIconOnPath(Canvas canvas, Path path, IconData icon, Color color) {
Rect bounds = path.getBounds();
TextPainter tp = TextPainter(text: TextSpan(text: String.fromCharCode(icon.codePoint), style: TextStyle(color: color, fontSize: 16, fontFamily: icon.fontFamily, package: icon.fontPackage)), textDirection: TextDirection.ltr)..layout();
tp.paint(canvas, Offset(bounds.center.dx - tp.width / 2, bounds.center.dy - tp.height / 2));
} }
void _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) { void _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) {
Paint crackPaint = Paint() Paint crackPaint = Paint()..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink))..strokeWidth = 3.0..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0);
..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink))
..strokeWidth = 3.0
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0);
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);
double dx = p2.dx - p1.dx; double dy = p2.dy - p1.dy;
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = sqrt(dx * dx + dy * dy);
double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x); if (len == 0) return;
double ndx = dx / len; double ndy = dy / len;
Path crack = Path()..moveTo(p1.dx, p1.dy); Path crack = Path()..moveTo(p1.dx, p1.dy);
int zigzags = 6; for (int i = 1; i < 6; i++) {
for (int i=1; i<zigzags; i++) {
double d = len * (i / zigzags);
Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
double offset = (i % 2 == 0 ? 3.0 : -3.0); double offset = (i % 2 == 0 ? 3.0 : -3.0);
crack.lineTo(basePt.dx + perp.x * offset, basePt.dy + perp.y * offset); crack.lineTo(p1.dx + ndx * (len * (i / 6)) + (-ndy) * offset, p1.dy + ndy * (len * (i / 6)) + ndx * offset);
} }
crack.lineTo(p2.dx, p2.dy); crack.lineTo(p2.dx, p2.dy);
canvas.drawPath(crack, crackPaint); canvas.drawPath(crack, crackPaint);
} }
void _drawArcadeBox(Canvas canvas, Rect rect, Color color) {
double pixelSize = 4.0; Paint paint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.fill;
for (double y = rect.top; y < rect.bottom; y += pixelSize) {
for (double x = rect.left; x < rect.right; x += pixelSize) {
int xi = ((x - rect.left) / pixelSize).floor(); int yi = ((y - rect.top) / pixelSize).floor();
if ((xi + yi) % 2 == 0) canvas.drawRect(Rect.fromLTWH(x, y, pixelSize, pixelSize), paint);
}
}
canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.white.withOpacity(0.4)..style = PaintingStyle.stroke..strokeWidth = 2.0);
}
void _drawGrimorioBox(Canvas canvas, Rect rect, Color color) {
canvas.drawRect(rect, Paint()..color = color.withOpacity(0.15)..style=PaintingStyle.fill);
Offset c = rect.center; double r = rect.width * 0.35;
Paint linePaint = Paint()..color = color.withOpacity(0.8)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
canvas.drawCircle(c, r, linePaint); canvas.drawCircle(c, r * 0.8, linePaint..strokeWidth = 0.5);
Path p = Path();
for(int i=0; i<3; i++) {
double a = -pi/2 + i * 2*pi/3; Offset pt = Offset(c.dx + r*cos(a), c.dy + r*sin(a));
if(i==0) p.moveTo(pt.dx, pt.dy); else p.lineTo(pt.dx, pt.dy);
}
p.close(); canvas.drawPath(p, linePaint..strokeWidth = 1.0);
}
void _drawArcadeLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
double pixelSize = 6.0; Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized();
Paint paint = Paint()..color = isConquered ? color : color.withOpacity(0.15)..style = PaintingStyle.fill;
Paint highlight = Paint()..color = Colors.white.withOpacity(0.6)..style = PaintingStyle.fill;
for(double d = 0; d <= len; d += pixelSize + 1.0) {
Offset pt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
canvas.drawRect(Rect.fromCenter(center: pt, width: pixelSize, height: pixelSize), paint);
if (isConquered && (d / (pixelSize+1.0)).floor() % 3 == 0) canvas.drawRect(Rect.fromCenter(center: pt - const Offset(1,1), width: pixelSize*0.4, height: pixelSize*0.4), highlight);
}
if (isLastMove && isConquered) canvas.drawRect(Rect.fromPoints(p1, p2).inflate(4.0), Paint()..color = Colors.white.withOpacity(blinkValue*0.4)..style=PaintingStyle.stroke..strokeWidth=2.0);
}
void _drawGrimorioLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
if (!isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.15)..strokeWidth = 2.0..strokeCap = StrokeCap.round); return; }
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.6)..strokeWidth = 5.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(0.7)..strokeWidth = 1.5..strokeCap = StrokeCap.round);
int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed);
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);
Path thread1 = Path(); Path thread2 = Path(); int segments = 15; double step = len / segments;
double phaseOffset = (isLastMove ? blinkValue * pi * 4 : 0) + rand.nextDouble()*pi;
for(int i = 0; i <= segments; i++) {
double d = i * step; Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
double amplitude = 3.5; double wave1 = sin(d * 0.15 + phaseOffset) * amplitude; double wave2 = cos(d * 0.15 + phaseOffset) * amplitude;
Offset pt1 = basePt + Offset(perp.x * wave1, perp.y * wave1); Offset pt2 = basePt + Offset(perp.x * wave2, perp.y * wave2);
if (i == 0) { thread1.moveTo(pt1.dx, pt1.dy); thread2.moveTo(pt2.dx, pt2.dy); } else { thread1.lineTo(pt1.dx, pt1.dy); thread2.lineTo(pt2.dx, pt2.dy); }
}
Paint threadPaint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
canvas.drawPath(thread1, threadPaint); canvas.drawPath(thread2, threadPaint..color = Colors.white.withOpacity(0.5));
}
void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) {
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 path = Path(); Rect rect = baseRect.deflate(4.0); int numZigs = 15 + rand.nextInt(6); double stepY = rect.height / numZigs;
path.moveTo(rect.left + rand.nextDouble() * 5, rect.top + rand.nextDouble() * 5);
for (int i = 1; i <= numZigs; i++) { double targetX = (i % 2 != 0) ? rect.right + (rand.nextDouble() * 4 - 2) : rect.left + (rand.nextDouble() * 4 - 2); double targetY = rect.top + stepY * i + (rand.nextDouble() - 0.5) * 3; double ctrlX = rect.center.dx + (rand.nextDouble() - 0.5) * 20; double ctrlY = targetY - stepY / 2; path.quadraticBezierTo(ctrlX, ctrlY, targetX, targetY); }
canvas.drawPath(path, paint);
}
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);
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));
if (isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isLastMove ? 0.7 + (0.3 * blinkValue) : 0.7)..strokeWidth = mainWidth * 2..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
canvas.drawLine(p1, p2, Paint()..color = coreColor..strokeWidth = mainWidth..strokeCap = StrokeCap.round);
}
void _drawWobblyLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
final random = Random((p1.dx + p1.dy + p2.dx + p2.dy).toInt()); final dx = p2.dx - p1.dx; final dy = p2.dy - p1.dy;
double strokeW = isConquered ? (isLastMove ? 4.5 + (2.0 * blinkValue) : 4.5) : 2.0;
final basePaint = Paint()..color = color..strokeWidth = strokeW..style = PaintingStyle.stroke..strokeCap = StrokeCap.round;
final mid1 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 8, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 8); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid1.dx, mid1.dy, p2.dx, p2.dy), basePaint);
final mid2 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 6, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 6); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid2.dx, mid2.dy, p2.dx, p2.dy), basePaint..strokeWidth = strokeW * 0.5..color = color.withOpacity(0.8));
}
@override bool shouldRepaint(covariant BoardPainter oldDelegate) => true; @override bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
}
class Vector2 {
final double x, y; Vector2(this.x, this.y); double get length => sqrt(x * x + y * y);
Vector2 normalized() { double l = length; return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l); }
} }

View file

@ -5,16 +5,15 @@
import 'dart:ui'; import 'dart:ui';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../logic/game_controller.dart'; import '../../logic/game_controller.dart';
import '../../core/theme_manager.dart'; import '../../core/theme_manager.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import '../../models/game_board.dart';
import 'board_painter.dart'; import 'board_painter.dart';
import 'score_board.dart'; import 'score_board.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../services/storage_service.dart';
TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) { TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) {
if (themeType == AppThemeType.doodle) { if (themeType == AppThemeType.doodle) {
@ -26,8 +25,6 @@ TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) {
)); ));
} else if (themeType == AppThemeType.grimorio) { } else if (themeType == AppThemeType.grimorio) {
return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold)); return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold));
} else if (themeType == AppThemeType.music) {
return GoogleFonts.audiowide(textStyle: baseStyle.copyWith(letterSpacing: 1.5));
} }
return baseStyle; return baseStyle;
} }
@ -48,6 +45,8 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
bool _wasSetupPhase = false; bool _wasSetupPhase = false;
Player _lastJokerTurn = Player.red; Player _lastJokerTurn = Player.red;
double _cameraAngle = 0.0; // La nostra telecamera fluida a 360 gradi
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -57,93 +56,20 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
@override @override
void dispose() { _blinkController.dispose(); super.dispose(); } void dispose() { _blinkController.dispose(); super.dispose(); }
// --- DIALOG: CONFERMA USCITA (ANTI-FUGA) AGGIORNATO ---
void _showExitConfirmationDialog(BuildContext context, GameController gameController, ThemeColors theme, AppThemeType themeType) {
// Determiniamo se è una partita locale (no CPU e no Online)
bool isLocalMatch = !gameController.isOnline && !gameController.isVsCPU;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: theme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: theme.playerRed, width: 2),
),
title: Row(
children: [
Icon(Icons.warning_amber_rounded, color: theme.playerRed),
const SizedBox(width: 10),
Expanded(
child: Text(
"ABBANDONARE?",
style: _getTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 18))
)
),
],
),
content: Text(
isLocalMatch
? "Sei sicuro di voler interrompere la partita locale in corso?" // Testo per partite in locale
: "Se esci ora, la partita verrà registrata automaticamente come una SCONFITTA.\n\nSei sicuro di voler fuggire?", // Testo minaccioso per partite classificate
style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 15, height: 1.4)),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text("ANNULLA", style: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontWeight: FontWeight.bold))),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.playerRed,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
// 1. Assegna la sconfitta SOLO se non è una partita in locale!
if (!isLocalMatch) {
StorageService.instance.addLoss();
}
// 2. Disconnette e pulisce
gameController.disconnectOnlineGame();
// 3. Chiude il dialog
Navigator.pop(ctx);
// 4. Torna al menu
Navigator.pop(context);
},
child: Text("SÌ, ESCI", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))),
),
],
),
);
}
void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) { void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) {
_gameOverDialogShown = true; _gameOverDialogShown = true;
showDialog( showDialog(
barrierDismissible: false, barrierDismissible: false, context: context,
context: context,
builder: (dialogContext) => Consumer<GameController>( builder: (dialogContext) => Consumer<GameController>(
builder: (context, controller, child) { builder: (context, controller, child) {
if (!controller.isGameOver) { if (!controller.isGameOver) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_gameOverDialogShown) { _gameOverDialogShown = false; if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext); } });
if (_gameOverDialogShown) {
_gameOverDialogShown = false;
if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext);
}
});
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
int red = controller.board.scoreRed; int blue = controller.board.scoreBlue; int red = controller.board.scoreRed; int blue = controller.board.scoreBlue;
bool playerBeatCPU = controller.isVsCPU && red > blue; bool playerBeatCPU = controller.isVsCPU && red > blue;
String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : "TU";
String myName = StorageService.instance.playerName.toUpperCase(); String nameBlue = controller.isOnline ? controller.onlineGuestName.toUpperCase() : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU");
if (myName.isEmpty) myName = "TU";
String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : myName;
String nameBlue = controller.isOnline ? controller.onlineGuestName.toUpperCase() : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? "VERDE" : "BLU");
if (controller.isVsCPU) nameBlue = "CPU"; if (controller.isVsCPU) nameBlue = "CPU";
String winnerText = ""; Color winnerColor = theme.text; String winnerText = ""; Color winnerColor = theme.text;
@ -152,8 +78,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
else { winnerText = "PAREGGIO!"; winnerColor = theme.text; } else { winnerText = "PAREGGIO!"; winnerColor = theme.text; }
return AlertDialog( return AlertDialog(
backgroundColor: theme.background, backgroundColor: theme.background, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)),
title: Text("FINE PARTITA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22))), title: Text("FINE PARTITA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22))),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -161,83 +86,49 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
Text(winnerText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))), Text(winnerText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))),
const SizedBox(height: 20), const SizedBox(height: 20),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)),
decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)), child: Row(
child: FittedBox( mainAxisSize: MainAxisSize.min,
fit: BoxFit.scaleDown, children: [
child: Row( Text("$nameRed: $red", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))),
mainAxisSize: MainAxisSize.min, Text(" - ", style: _getTextStyle(themeType, TextStyle(fontSize: 18, color: theme.text))),
children: [ Text("$nameBlue: $blue", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))),
Text("$nameRed: $red", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))), ],
Text(" - ", style: _getTextStyle(themeType, TextStyle(fontSize: 18, color: theme.text))),
Text("$nameBlue: $blue", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))),
],
),
), ),
), ),
if (controller.lastMatchXP > 0) ...[ if (controller.lastMatchXP > 0) ...[
const SizedBox(height: 15), const SizedBox(height: 15),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(color: Colors.green.withOpacity(0.15), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.greenAccent, width: 1.5), boxShadow: themeType == AppThemeType.cyberpunk ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : []),
color: Colors.green.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.greenAccent, width: 1.5),
boxShadow: (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : [],
),
child: Text("+ ${controller.lastMatchXP} XP", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))), child: Text("+ ${controller.lastMatchXP} XP", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))),
), ),
], ],
if (controller.isVsCPU) ...[ if (controller.isVsCPU) ...[
const SizedBox(height: 15), const SizedBox(height: 15),
Text("Difficoltà CPU: Livello ${controller.cpuLevel}", style: _getTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7)))), Text("Difficoltà CPU: Livello ${controller.cpuLevel}", style: _getTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7)))),
], ],
if (controller.isOnline) ...[ if (controller.isOnline) ...[
const SizedBox(height: 20), const SizedBox(height: 20),
if (controller.rematchRequested && !controller.opponentWantsRematch) if (controller.rematchRequested && !controller.opponentWantsRematch) Text("In attesa di $nameBlue...", style: _getTextStyle(themeType, const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))),
Text("In attesa di $nameBlue...", style: _getTextStyle(themeType, const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))), if (controller.opponentWantsRematch && !controller.rematchRequested) Text("$nameBlue vuole la rivincita!", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold))),
if (controller.opponentWantsRematch && !controller.rematchRequested) if (controller.rematchRequested && controller.opponentWantsRematch) Text("Avvio nuova partita...", style: _getTextStyle(themeType, const TextStyle(color: Colors.green, fontWeight: FontWeight.bold))),
Text("$nameBlue vuole la rivincita!", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold))),
if (controller.rematchRequested && controller.opponentWantsRematch)
Text("Avvio nuova partita...", style: _getTextStyle(themeType, const TextStyle(color: Colors.green, fontWeight: FontWeight.bold))),
] ]
], ],
), ),
actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10), actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10), actionsAlignment: MainAxisAlignment.center,
actionsAlignment: MainAxisAlignment.center,
actions: [ actions: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (playerBeatCPU) if (playerBeatCPU)
ElevatedButton( ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: () { controller.increaseLevelAndRestart(); }, child: Text("PROSSIMO LIVELLO ➔", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))))
style: ElevatedButton.styleFrom(backgroundColor: winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
onPressed: () { controller.increaseLevelAndRestart(); },
child: Text("PROSSIMO LIVELLO ➔", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))),
)
else if (controller.isOnline) else if (controller.isOnline)
ElevatedButton( ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: controller.rematchRequested ? Colors.grey : (winnerColor == theme.text ? theme.playerBlue : winnerColor), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: controller.rematchRequested ? null : () { controller.requestRematch(); }, child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0))))
style: ElevatedButton.styleFrom(backgroundColor: controller.rematchRequested ? Colors.grey : (winnerColor == theme.text ? theme.playerBlue : winnerColor), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
onPressed: controller.rematchRequested ? null : () { controller.requestRematch(); },
child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0))),
)
else else
ElevatedButton( ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.isTimeMode); }, child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)))),
style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.timeModeSetting); },
child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2))),
),
const SizedBox(height: 12), const SizedBox(height: 12),
OutlinedButton( OutlinedButton(style: OutlinedButton.styleFrom(foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () { if (controller.isOnline) controller.disconnectOnlineGame(); _gameOverDialogShown = false; Navigator.pop(dialogContext); Navigator.pop(context); }, child: Text("TORNA AL MENU", style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)))),
style: OutlinedButton.styleFrom(foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () {
if (controller.isOnline) controller.disconnectOnlineGame();
_gameOverDialogShown = false;
Navigator.pop(dialogContext); Navigator.pop(context);
},
child: Text("TORNA AL MENU", style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))),
),
], ],
) )
], ],
@ -248,62 +139,19 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
} }
Widget _buildThemedJokerMessage(ThemeColors theme, AppThemeType themeType, GameController gameController) { Widget _buildThemedJokerMessage(ThemeColors theme, AppThemeType themeType, GameController gameController) {
String titleText = ""; String titleText = ""; String subtitleText = "";
String subtitleText = ""; if (gameController.isOnline) { titleText = gameController.myJokerPlaced ? "In attesa dell'avversario..." : "Nascondi il tuo Jolly!"; subtitleText = gameController.myJokerPlaced ? "" : "(Tocca qui per nascondere)"; }
else if (gameController.isVsCPU) { titleText = "Nascondi il tuo Jolly!"; subtitleText = "(Tocca qui per nascondere)"; }
else { String pName = gameController.jokerTurn == Player.red ? "ROSSO" : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU"); titleText = "TURNO GIOCATORE $pName"; subtitleText = "Passa il dispositivo.\nL'avversario NON deve guardare!\n\n(Tocca qui quando sei pronto)"; }
if (gameController.isOnline) { Widget content = Padding(padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 25), child: Column(mainAxisSize: MainAxisSize.min, children: [Icon(ThemeIcons.joker(themeType), color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.yellowAccent : theme.playerBlue, size: 50), const SizedBox(height: 15), Text(titleText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.black87 : theme.text, fontSize: 20, fontWeight: FontWeight.bold))), const SizedBox(height: 25), Text(subtitleText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.6), fontSize: 12, height: 1.5)))]));
titleText = gameController.myJokerPlaced ? "In attesa dell'avversario..." : "Nascondi il tuo Jolly!";
subtitleText = gameController.myJokerPlaced ? "" : "(Tocca qui per nascondere)";
} else if (gameController.isVsCPU) {
titleText = "Nascondi il tuo Jolly!";
subtitleText = "(Tocca qui per nascondere)";
} else {
String pName = gameController.jokerTurn == Player.red ? "ROSSO" : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? "VERDE" : "BLU");
titleText = "TURNO GIOCATORE $pName";
subtitleText = "Passa il dispositivo.\nL'avversario NON deve guardare!\n\n(Tocca qui quando sei pronto)";
}
Widget content = Padding( if (themeType == AppThemeType.cyberpunk) return Container(decoration: BoxDecoration(color: Colors.black.withOpacity(0.9), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.yellowAccent, width: 2), boxShadow: [const BoxShadow(color: Colors.yellowAccent, blurRadius: 15, spreadRadius: 0)]), child: content);
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 25), 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);
child: Column( 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);
mainAxisSize: MainAxisSize.min, 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);
children: [ else if (themeType == AppThemeType.grimorio) return Container(decoration: BoxDecoration(color: const Color(0xFF2C1E3D), borderRadius: BorderRadius.circular(30), border: Border.all(color: const Color(0xFFBCAAA4), width: 3), boxShadow: [BoxShadow(color: Colors.deepPurpleAccent.withOpacity(0.5), blurRadius: 20, spreadRadius: 5)]), child: content);
Icon(ThemeIcons.joker(themeType), color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.yellowAccent : theme.playerBlue, size: 50), else return Container(decoration: BoxDecoration(color: theme.background, borderRadius: BorderRadius.circular(20), border: Border.all(color: theme.gridLine, width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.15), blurRadius: 20, offset: const Offset(0, 10))]), child: content);
const SizedBox(height: 15),
Text(
titleText,
textAlign: TextAlign.center,
style: _getTextStyle(themeType, TextStyle(
color: themeType == AppThemeType.doodle ? Colors.black87 : theme.text,
fontSize: 20,
fontWeight: FontWeight.bold,
)),
),
const SizedBox(height: 25),
Text(
subtitleText,
textAlign: TextAlign.center,
style: _getTextStyle(themeType, TextStyle(
color: themeType == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.6),
fontSize: 12,
height: 1.5
)),
),
],
),
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
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) {
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.arcade) {
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) {
return Container(decoration: BoxDecoration(color: const Color(0xFF2C1E3D), borderRadius: BorderRadius.circular(30), border: Border.all(color: const Color(0xFFBCAAA4), width: 3), boxShadow: [BoxShadow(color: Colors.deepPurpleAccent.withOpacity(0.5), blurRadius: 20, spreadRadius: 5)]), child: content);
} else {
return Container(decoration: BoxDecoration(color: theme.background.withOpacity(0.95), borderRadius: BorderRadius.circular(20), border: Border.all(color: theme.gridLine.withOpacity(0.5), width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 10))]), child: content);
}
} }
@override @override
@ -313,68 +161,29 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
final theme = themeManager.currentColors; final theme = themeManager.currentColors;
final gameController = context.watch<GameController>(); final gameController = context.watch<GameController>();
if (gameController.isSetupPhase && !_wasSetupPhase) { if (gameController.isSetupPhase && !_wasSetupPhase) { _hideJokerMessage = false; _lastJokerTurn = Player.red; }
_hideJokerMessage = false; else if (gameController.isSetupPhase && gameController.jokerTurn != _lastJokerTurn) { _hideJokerMessage = false; _lastJokerTurn = gameController.jokerTurn; }
_lastJokerTurn = Player.red;
} else if (gameController.isSetupPhase && gameController.jokerTurn != _lastJokerTurn) {
_hideJokerMessage = false;
_lastJokerTurn = gameController.jokerTurn;
}
_wasSetupPhase = gameController.isSetupPhase; _wasSetupPhase = gameController.isSetupPhase;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (gameController.opponentLeft && !_opponentLeftDialogShown) { if (gameController.opponentLeft && !_opponentLeftDialogShown) {
_opponentLeftDialogShown = true; _opponentLeftDialogShown = true;
showDialog( showDialog(barrierDismissible: false, context: context, builder: (dialogContext) => AlertDialog(backgroundColor: theme.background, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))), content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))), actionsAlignment: MainAxisAlignment.center, actions: [ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); }, child: Text("MENU PRINCIPALE", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))))]));
barrierDismissible: false,
context: context,
builder: (dialogContext) => AlertDialog(
backgroundColor: theme.background,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))),
content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))),
actionsAlignment: MainAxisAlignment.center,
actions: [
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); },
child: Text("MENU PRINCIPALE", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))),
)
],
)
);
} else if (gameController.board.isGameOver && !_gameOverDialogShown) { } else if (gameController.board.isGameOver && !_gameOverDialogShown) {
_showGameOverDialog(context, gameController, theme, themeType); _showGameOverDialog(context, gameController, theme, themeType);
} }
}); });
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.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';
Color indicatorColor = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.white : Colors.black; Color indicatorColor = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.white : Colors.black;
Widget emojiBar = const SizedBox(); Widget emojiBar = const SizedBox();
if (gameController.isOnline && !gameController.isGameOver) { if (gameController.isOnline && !gameController.isGameOver) {
final List<String> emojis = ['😂', '😡', '😱', '🥳', '👀']; final List<String> emojis = ['😂', '😡', '😱', '🥳', '👀'];
emojiBar = Container( emojiBar = Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration(color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.black.withOpacity(0.6) : Colors.white.withOpacity(0.8), borderRadius: BorderRadius.circular(30), border: Border.all(color: themeType == AppThemeType.cyberpunk ? theme.playerBlue.withOpacity(0.3) : Colors.black12, width: 2)), child: Row(mainAxisSize: MainAxisSize.min, children: emojis.map((e) => GestureDetector(onTap: () => gameController.sendReaction(e), child: Padding(padding: const EdgeInsets.symmetric(horizontal: 6), child: Text(e, style: const TextStyle(fontSize: 22))))).toList()));
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.black.withOpacity(0.6) : Colors.white.withOpacity(0.8),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music ? theme.playerBlue.withOpacity(0.3) : Colors.white24, width: 2),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: emojis.map((e) => GestureDetector(
onTap: () => gameController.sendReaction(e),
child: Padding(padding: const EdgeInsets.symmetric(horizontal: 6), child: Text(e, style: const TextStyle(fontSize: 22))),
)).toList(),
),
);
} }
Widget gameContent = SafeArea( Widget gameContent = SafeArea(
@ -386,7 +195,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
Expanded( Expanded(
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 2.0), padding: const EdgeInsets.all(10.0),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
int cols = gameController.board.columns + 1; int cols = gameController.board.columns + 1;
@ -399,41 +208,31 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
return SizedBox( return SizedBox(
width: actualWidth, height: actualHeight, width: actualWidth, height: actualHeight,
child: Stack( child: GestureDetector(
children: [ behavior: HitTestBehavior.opaque,
Positioned.fill( onTapUp: (details) => _handleTap(details.localPosition, actualWidth, actualHeight, gameController, themeType),
child: ClipPath( onPanUpdate: (details) {
clipper: _ArenaClipper(gameController.board), if (gameController.board.shape == ArenaShape.pyramid3D) {
child: BackdropFilter( setState(() {
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), _cameraAngle -= details.delta.dx * 0.015;
child: Container( });
color: themeType == AppThemeType.doodle }
? Colors.black.withOpacity(0.05) },
: Colors.white.withOpacity(0.12), child: AnimatedBuilder(
animation: _blinkController,
builder: (context, child) {
return CustomPaint(
size: Size(actualWidth, actualHeight),
painter: BoardPainter(
board: gameController.board, theme: theme, themeType: themeType,
blinkValue: _blinkController.value, isOnline: gameController.isOnline,
isVsCPU: gameController.isVsCPU, isSetupPhase: gameController.isSetupPhase,
myPlayer: gameController.myPlayer, jokerTurn: gameController.jokerTurn,
cameraAngle: _cameraAngle,
), ),
), );
), }
), ),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (details) => _handleTap(details.localPosition, actualWidth, actualHeight, gameController, themeType),
child: AnimatedBuilder(
animation: _blinkController,
builder: (context, child) {
return CustomPaint(
size: Size(actualWidth, actualHeight),
painter: BoardPainter(
board: gameController.board, theme: theme, themeType: themeType,
blinkValue: _blinkController.value, isOnline: gameController.isOnline,
isVsCPU: gameController.isVsCPU, isSetupPhase: gameController.isSetupPhase,
myPlayer: gameController.myPlayer, jokerTurn: gameController.jokerTurn,
),
);
}
),
),
],
), ),
); );
} }
@ -443,142 +242,49 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
), ),
Padding( Padding(
padding: const EdgeInsets.only(bottom: 10.0, left: 20.0, right: 20.0, top: 5.0), padding: const EdgeInsets.only(bottom: 20.0, left: 20.0, right: 20.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (gameController.isVsCPU) if (gameController.isVsCPU)
Container( Container(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(color: indicatorColor.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: indicatorColor.withOpacity(0.3))), child: Row(mainAxisSize: MainAxisSize.min, children: [Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8), Text("LIVELLO CPU: ${gameController.cpuLevel}", style: _getTextStyle(themeType, TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1.0)))]))
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(color: indicatorColor.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: indicatorColor.withOpacity(0.3))),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8),
Text("LIVELLO CPU: ${gameController.cpuLevel}", style: _getTextStyle(themeType, TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1.0))),
],
),
)
else else
emojiBar, emojiBar,
Container( Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 5)]), child: TextButton.icon(style: TextButton.styleFrom(backgroundColor: bgImage != null || themeType == AppThemeType.arcade ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))), icon: Icon(Icons.exit_to_app, color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, size: 20), onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); }, label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))))),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 5)]),
child: TextButton.icon(
style: TextButton.styleFrom(backgroundColor: bgImage != null || themeType == AppThemeType.arcade ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))),
icon: Icon(Icons.exit_to_app, color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, size: 20),
// --- NUOVO ON PRESSED ANTI-FUGA ---
onPressed: () {
if (!gameController.isGameOver && !gameController.isSetupPhase) {
_showExitConfirmationDialog(context, gameController, theme, themeType);
} else {
gameController.disconnectOnlineGame();
Navigator.pop(context);
}
},
// ------------------------
label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))),
),
),
], ],
), ),
) )
], ],
), ),
if (gameController.myReaction != null) if (gameController.board.shape == ArenaShape.pyramid3D)
Positioned(top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!)), Positioned(top: 80, left: 0, right: 0, child: Center(child: Text("Scorri in orizzontale per ruotare", style: TextStyle(color: Colors.white.withOpacity(0.5), fontStyle: FontStyle.italic, fontWeight: FontWeight.bold, letterSpacing: 1.5)))),
if (gameController.opponentReaction != null)
Positioned(top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!)), if (gameController.myReaction != null) Positioned(top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!)),
if (gameController.opponentReaction != null) Positioned(top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!)),
], ],
), ),
); );
// --- NUOVA LOGICA: Impedisce la chiusura accidentale o la fuga (Tasto Indietro) ---
bool shouldConfirmExit = !gameController.isGameOver && !gameController.isSetupPhase;
return PopScope( return PopScope(
canPop: !shouldConfirmExit, canPop: true, onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); },
onPopInvoked: (didPop) {
if (didPop) {
gameController.disconnectOnlineGame();
return;
}
if (shouldConfirmExit) {
_showExitConfirmationDialog(context, gameController, theme, themeType);
}
},
child: Scaffold( child: Scaffold(
backgroundColor: themeType == AppThemeType.doodle ? Colors.white : (bgImage != null ? Colors.transparent : theme.background), backgroundColor: bgImage != null ? Colors.transparent : theme.background,
body: Stack( body: CustomPaint(
children: [ painter: themeType == AppThemeType.minimal ? FullScreenGridPainter(Colors.black.withOpacity(0.06)) : null,
Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background), child: Container(
decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover, colorFilter: themeType == AppThemeType.doodle ? ColorFilter.mode(Colors.white.withOpacity(0.7), BlendMode.lighten) : null)) : null,
if (themeType == AppThemeType.doodle) child: Stack(
Positioned.fill( children: [
child: CustomPaint( if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5 && !gameController.isSetupPhase) Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed, themeType: themeType)),
painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)), if (gameController.effectText.isNotEmpty) Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor, themeType: themeType)),
), Positioned.fill(child: gameContent),
), if (gameController.isSetupPhase && !_hideJokerMessage) Positioned.fill(child: Container(color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.black : theme.background.withOpacity(0.98), child: Center(child: Padding(padding: const EdgeInsets.symmetric(horizontal: 30.0), child: GestureDetector(onTap: () { setState(() { _hideJokerMessage = true; }); }, child: Material(color: Colors.transparent, child: _buildThemedJokerMessage(theme, themeType, gameController))))))),
if (gameController.isGameOver && gameController.board.scoreRed != gameController.board.scoreBlue) Positioned.fill(child: IgnorePointer(child: WinnerVFXOverlay(winnerColor: gameController.board.scoreRed > gameController.board.scoreBlue ? theme.playerRed : theme.playerBlue, themeType: themeType))),
if (bgImage != null) ],
Positioned.fill( ),
child: Container( ),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(bgImage!),
fit: BoxFit.cover,
colorFilter: themeType == AppThemeType.doodle
? ColorFilter.mode(Colors.white.withOpacity(0.5), BlendMode.lighten)
: null,
),
),
),
),
if (bgImage != null && (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music || themeType == AppThemeType.arcade || themeType == AppThemeType.grimorio))
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter, end: Alignment.bottomCenter,
colors: [Colors.black.withOpacity(0.4), Colors.black.withOpacity(0.8)]
)
),
),
),
if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5 && !gameController.isSetupPhase)
Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed, themeType: themeType)),
if (gameController.effectText.isNotEmpty)
Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor, themeType: themeType)),
Positioned.fill(child: gameContent),
if (gameController.isSetupPhase && !_hideJokerMessage)
Positioned.fill(
child: Container(
color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music
? Colors.black.withOpacity(0.98)
: theme.background.withOpacity(0.98),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30.0),
child: GestureDetector(
onTap: () { setState(() { _hideJokerMessage = true; }); },
child: Material(color: Colors.transparent, child: _buildThemedJokerMessage(theme, themeType, gameController)),
),
),
),
),
),
if (gameController.isGameOver && gameController.board.scoreRed != gameController.board.scoreBlue)
Positioned.fill(child: IgnorePointer(child: WinnerVFXOverlay(winnerColor: gameController.board.scoreRed > gameController.board.scoreBlue ? theme.playerRed : theme.playerBlue, themeType: themeType))),
],
), ),
), ),
); );
@ -587,21 +293,48 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
void _handleTap(Offset tapPos, double width, double height, GameController controller, AppThemeType themeType) { void _handleTap(Offset tapPos, double width, double height, GameController controller, AppThemeType themeType) {
final board = controller.board; final board = controller.board;
if (board.isGameOver) return; if (board.isGameOver) return;
int cols = board.columns + 1; double spacing = width / cols; double offset = spacing / 2;
BoardPainter dummyPainter = BoardPainter(
board: board, theme: AppColors.minimal, themeType: AppThemeType.minimal,
isOnline: false, isVsCPU: false, isSetupPhase: false,
myPlayer: Player.red, jokerTurn: Player.red,
cameraAngle: _cameraAngle,
);
if (controller.isSetupPhase) { if (controller.isSetupPhase) {
int bx = ((tapPos.dx - offset) / spacing).floor(); int by = ((tapPos.dy - offset) / spacing).floor(); var sortedBoxes = List<Box>.from(board.boxes);
controller.placeJoker(bx, by); return; sortedBoxes.sort((a,b) => dummyPainter.getDepth(b).compareTo(dummyPainter.getDepth(a)));
for (var box in sortedBoxes) {
if (box.type == BoxType.invisible) continue;
Offset p0 = dummyPainter.projectLogical(box.x.toDouble(), box.y.toDouble(), box.z.toDouble(), Size(width, height));
Offset p1 = dummyPainter.projectLogical(box.x + 1.0, box.y.toDouble(), box.z.toDouble(), Size(width, height));
Offset p2 = dummyPainter.projectLogical(box.x + 1.0, box.y + 1.0, box.z.toDouble(), Size(width, height));
Offset p3 = dummyPainter.projectLogical(box.x.toDouble(), box.y + 1.0, box.z.toDouble(), Size(width, height));
Path poly = Path()..moveTo(p0.dx, p0.dy)..lineTo(p1.dx, p1.dy)..lineTo(p2.dx, p2.dy)..lineTo(p3.dx, p3.dy)..close();
if (poly.contains(tapPos)) {
controller.placeJoker(box.x, box.y, bz: box.z);
return;
}
}
return;
} }
Line? closestLine; double minDistance = double.infinity; double maxTouchDistance = spacing * 0.4; Line? closestLine;
double minDistance = double.infinity;
double maxTouchDistance = 40.0;
for (var line in board.lines) { for (var line in board.lines) {
if (line.owner != Player.none || !line.isPlayable) continue; if (line.owner != Player.none || !line.isPlayable) continue;
Offset screenP1 = Offset(line.p1.x * spacing + offset, line.p1.y * spacing + offset);
Offset screenP2 = Offset(line.p2.x * spacing + offset, line.p2.y * spacing + offset); Offset screenP1 = dummyPainter.projectLogical(line.p1.x.toDouble(), line.p1.y.toDouble(), line.p1.z.toDouble(), Size(width, height));
Offset screenP2 = dummyPainter.projectLogical(line.p2.x.toDouble(), line.p2.y.toDouble(), line.p2.z.toDouble(), Size(width, height));
double dist = _distanceToSegment(tapPos, screenP1, screenP2); double dist = _distanceToSegment(tapPos, screenP1, screenP2);
if (dist < minDistance && dist < maxTouchDistance) { minDistance = dist; closestLine = line; } if (dist < minDistance && dist < maxTouchDistance) { minDistance = dist; closestLine = line; }
} }
if (closestLine != null) { controller.handleLineTap(closestLine, themeType); } if (closestLine != null) { controller.handleLineTap(closestLine, themeType); }
} }
@ -614,35 +347,6 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
} }
} }
// ===========================================================================
// CLIPPER MAGICO E ALTRI WIDGETS
// ===========================================================================
class _ArenaClipper extends CustomClipper<Path> {
final GameBoard board;
_ArenaClipper(this.board);
@override
Path getClip(Size size) {
int cols = board.columns + 1;
double spacing = size.width / cols;
double offset = spacing / 2;
Path path = Path();
for (var box in board.boxes) {
if (box.type != BoxType.invisible) {
path.addRect(Rect.fromLTWH(
box.x * spacing + offset,
box.y * spacing + offset,
spacing,
spacing
));
}
}
return path;
}
@override bool shouldReclip(covariant _ArenaClipper oldClipper) => true;
}
class _Particle { class _Particle {
double x, y, vx, vy, size, angle, spin; double x, y, vx, vy, size, angle, spin;
Color color; int type; Color color; int type;
@ -674,16 +378,16 @@ class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerPr
} }
void _initParticles(Size screenSize) { void _initParticles(Size screenSize) {
int particleCount = widget.themeType == AppThemeType.cyberpunk || widget.themeType == AppThemeType.music ? 150 : 100; int particleCount = widget.themeType == AppThemeType.cyberpunk ? 150 : 100;
if (widget.themeType == AppThemeType.arcade) particleCount = 80; if (widget.themeType == AppThemeType.arcade) particleCount = 80;
if (widget.themeType == AppThemeType.grimorio) particleCount = 120; if (widget.themeType == AppThemeType.grimorio) particleCount = 120;
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); }
for (int i = 0; i < particleCount; i++) { for (int i = 0; i < particleCount; i++) {
double speed = _rand.nextDouble() * 20 + 5; double speed = _rand.nextDouble() * 20 + 5;
@ -696,7 +400,8 @@ class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerPr
setState(() { setState(() {
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) { 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; }
@ -704,6 +409,7 @@ class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerPr
} }
}); });
} }
@override void dispose() { _vfxController.dispose(); super.dispose(); } @override void dispose() { _vfxController.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return CustomPaint(painter: _VFXPainter(particles: _particles, themeType: widget.themeType), child: Container()); } @override Widget build(BuildContext context) { return CustomPaint(painter: _VFXPainter(particles: _particles, themeType: widget.themeType), child: Container()); }
} }
@ -717,12 +423,14 @@ class _VFXPainter extends CustomPainter {
for (var p in particles) { for (var p in particles) {
if (p.size < 0.5) continue; if (p.size < 0.5) continue;
final paint = Paint()..color = p.color..style = PaintingStyle.fill; final paint = Paint()..color = p.color..style = PaintingStyle.fill;
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { paint.maskFilter = const MaskFilter.blur(BlurStyle.solid, 4.0); } if (themeType == AppThemeType.cyberpunk) { paint.maskFilter = const MaskFilter.blur(BlurStyle.solid, 4.0); }
canvas.save(); canvas.translate(p.x, p.y); canvas.rotate(p.angle); canvas.save(); canvas.translate(p.x, p.y); canvas.rotate(p.angle);
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) {
@ -744,6 +452,7 @@ class _BouncingEmoji extends StatefulWidget {
final String emoji; const _BouncingEmoji({required this.emoji}); final String emoji; const _BouncingEmoji({required this.emoji});
@override State<_BouncingEmoji> createState() => _BouncingEmojiState(); @override State<_BouncingEmoji> createState() => _BouncingEmojiState();
} }
class _BouncingEmojiState extends State<_BouncingEmoji> with SingleTickerProviderStateMixin { class _BouncingEmojiState extends State<_BouncingEmoji> with SingleTickerProviderStateMixin {
late AnimationController _ctrl; late Animation<double> _anim; late AnimationController _ctrl; late Animation<double> _anim;
@override void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 500))..repeat(reverse: true); _anim = Tween<double>(begin: -10, end: 10).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut)); } @override void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 500))..repeat(reverse: true); _anim = Tween<double>(begin: -10, end: 10).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut)); }

View file

@ -4,28 +4,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart'; // Import separati e puliti
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 '../../services/audio_service.dart'; import '../../services/audio_service.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import '../../services/storage_service.dart';
import '../home/dialog.dart'; // <--- IMPORTANTE: Importa il TutorialDialog
TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) {
if (themeType == AppThemeType.doodle) {
return GoogleFonts.permanentMarker(textStyle: baseStyle);
} else if (themeType == AppThemeType.arcade) {
return GoogleFonts.pressStart2p(textStyle: baseStyle.copyWith(
fontSize: baseStyle.fontSize != null ? baseStyle.fontSize! * 0.75 : null,
letterSpacing: 0.5,
));
} else if (themeType == AppThemeType.grimorio) {
return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold));
}
return baseStyle;
}
class ScoreBoard extends StatefulWidget { class ScoreBoard extends StatefulWidget {
const ScoreBoard({super.key}); const ScoreBoard({super.key});
@ -48,24 +32,21 @@ class _ScoreBoardState extends State<ScoreBoard> {
bool isRedTurn = controller.board.currentPlayer == Player.red; bool isRedTurn = controller.board.currentPlayer == Player.red;
bool isMuted = AudioService.instance.isMuted; bool isMuted = AudioService.instance.isMuted;
String myName = StorageService.instance.playerName.toUpperCase(); String nameRed = "ROSSO";
if (myName.isEmpty) myName = "TU"; String nameBlue = themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU";
String nameRed = myName;
String nameBlue = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU";
if (controller.isOnline) { if (controller.isOnline) {
nameRed = controller.onlineHostName.toUpperCase(); nameRed = controller.onlineHostName.toUpperCase();
nameBlue = controller.onlineGuestName.toUpperCase(); nameBlue = controller.onlineGuestName.toUpperCase();
} else if (controller.isVsCPU) { } else if (controller.isVsCPU) {
nameRed = myName; nameRed = "TU";
nameBlue = "CPU"; nameBlue = "CPU";
} }
return Container( return Container(
padding: const EdgeInsets.only(top: 10, bottom: 20, left: 20, right: 20), padding: const EdgeInsets.only(top: 10, bottom: 20, left: 20, right: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? theme.background : theme.background.withOpacity(0.95), color: theme.background.withOpacity(0.95),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.3), color: Colors.black.withOpacity(0.3),
@ -81,84 +62,33 @@ class _ScoreBoardState extends State<ScoreBoard> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_PlayerScore(color: theme.playerRed, score: redScore, isTurn: isRedTurn, textColor: theme.text, title: nameRed, themeType: themeType), _PlayerScore(color: theme.playerRed, score: redScore, isTurn: isRedTurn, textColor: theme.text, title: nameRed),
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
"TETRAQ", "TETRAQ",
style: _getTextStyle(themeType, TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: theme.text, color: theme.text,
letterSpacing: 4, letterSpacing: 4,
shadows: themeType == AppThemeType.doodle shadows: [Shadow(color: Colors.black.withOpacity(0.3), offset: const Offset(1, 2), blurRadius: 2)]
? [ )
// EFFETTO RILIEVO (Luce in alto a sx, ombra in basso a dx)
const Shadow(color: Colors.white, offset: Offset(-1.5, -1.5), blurRadius: 1),
Shadow(color: Colors.black.withOpacity(0.25), offset: const Offset(1.5, 1.5), blurRadius: 2),
]
: [Shadow(color: Colors.black.withOpacity(0.3), offset: const Offset(1, 2), blurRadius: 2)]
))
), ),
const SizedBox(height: 8), IconButton(
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up, color: theme.text.withOpacity(0.7)),
// --- ROW DEI PULSANTI AGGIORNATA --- onPressed: () {
Row( setState(() {
mainAxisSize: MainAxisSize.min, AudioService.instance.toggleMute();
children: [ });
// TASTO AUDIO CON CONTORNO },
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
setState(() {
AudioService.instance.toggleMute();
});
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.3), width: 1.5),
),
child: Icon(
isMuted ? Icons.volume_off : Icons.volume_up,
color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.8),
size: 16
),
),
),
const SizedBox(width: 10),
// TASTO INFORMAZIONI (TUTORIAL) CON CONTORNO
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
showDialog(context: context, builder: (ctx) => const TutorialDialog());
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.3), width: 1.5),
),
child: Icon(
Icons.info_outline,
color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.8),
size: 16
),
),
),
],
), ),
], ],
), ),
_PlayerScore(color: theme.playerBlue, score: blueScore, isTurn: !isRedTurn, textColor: theme.text, title: nameBlue, themeType: themeType), _PlayerScore(color: theme.playerBlue, score: blueScore, isTurn: !isRedTurn, textColor: theme.text, title: nameBlue),
], ],
), ),
); );
@ -171,16 +101,15 @@ class _PlayerScore extends StatelessWidget {
final bool isTurn; final bool isTurn;
final Color textColor; final Color textColor;
final String title; final String title;
final AppThemeType themeType;
const _PlayerScore({required this.color, required this.score, required this.isTurn, required this.textColor, required this.title, required this.themeType}); const _PlayerScore({required this.color, required this.score, required this.isTurn, required this.textColor, required this.title});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(title, style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: isTurn ? color : textColor.withOpacity(0.5), fontSize: 12))), Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: isTurn ? color : textColor.withOpacity(0.5), fontSize: 12)),
const SizedBox(height: 5), const SizedBox(height: 5),
AnimatedContainer( AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
@ -193,7 +122,7 @@ class _PlayerScore extends StatelessWidget {
BoxShadow(color: color.withOpacity(0.5), offset: const Offset(0, 4), blurRadius: 6) BoxShadow(color: color.withOpacity(0.5), offset: const Offset(0, 4), blurRadius: 6)
] : [], ] : [],
), ),
child: Text('$score', style: _getTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: isTurn ? Colors.white : textColor.withOpacity(0.5)))), child: Text('$score', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: isTurn ? Colors.white : textColor.withOpacity(0.5))),
), ),
], ],
); );

View file

@ -1,541 +0,0 @@
// ===========================================================================
// FILE: lib/ui/home/dialog.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
import '../../l10n/app_localizations.dart';
import '../../widgets/painters.dart';
import '../../widgets/cyber_border.dart';
import '../../services/storage_service.dart';
// ===========================================================================
// 1. DIALOGO MISSIONI (QUESTS)
// ===========================================================================
class QuestsDialog extends StatelessWidget {
const QuestsDialog({super.key});
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
final loc = AppLocalizations.of(context)!;
return FutureBuilder<SharedPreferences>(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox();
final prefs = snapshot.data!;
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Container(
padding: const EdgeInsets.all(25.0),
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
borderRadius: BorderRadius.circular(25),
border: Border.all(color: theme.playerBlue.withOpacity(0.5), width: 2),
boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.2), blurRadius: 20, spreadRadius: 5)]
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.assignment_turned_in, size: 50, color: theme.playerBlue),
const SizedBox(height: 10),
Text(loc.questsTitle, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))),
const SizedBox(height: 25),
...List.generate(3, (index) {
int i = index + 1;
int type = prefs.getInt('q${i}_type') ?? 0;
int prog = prefs.getInt('q${i}_prog') ?? 0;
int target = prefs.getInt('q${i}_target') ?? 1;
String title = "";
IconData icon = Icons.star;
if (type == 0) { title = "Vinci partite Online"; icon = Icons.public; }
else if (type == 1) { title = "Vinci contro la CPU"; icon = Icons.smart_toy; }
else { title = "Gioca in Arene Speciali"; icon = Icons.extension; }
bool completed = prog >= target;
double percent = (prog / target).clamp(0.0, 1.0);
return Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: completed ? Colors.green.withOpacity(0.1) : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: completed ? Colors.green : theme.gridLine.withOpacity(0.3)),
),
child: Row(
children: [
Icon(icon, color: completed ? Colors.green : theme.text.withOpacity(0.6), size: 30),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: completed ? Colors.green : theme.text))),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: LinearProgressIndicator(value: percent, backgroundColor: theme.gridLine.withOpacity(0.2), color: completed ? Colors.green : theme.playerBlue, minHeight: 8),
)
],
),
),
const SizedBox(width: 10),
Text("$prog / $target", style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.6)))),
],
),
);
}),
const SizedBox(height: 15),
SizedBox(
width: double.infinity, height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () => Navigator.pop(context),
child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
)
],
),
),
);
}
);
}
}
// ===========================================================================
// 2. DIALOGO CLASSIFICA (LEADERBOARD) CON CALLBACK SFIDA
// ===========================================================================
class LeaderboardDialog extends StatelessWidget {
final Function(String uid, String name)? onChallenge;
const LeaderboardDialog({super.key, this.onChallenge});
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
final loc = AppLocalizations.of(context)!;
Widget content = Container(
padding: const EdgeInsets.all(20.0),
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
borderRadius: BorderRadius.circular(25),
border: Border.all(color: Colors.amber.withOpacity(0.8), width: 2),
boxShadow: [BoxShadow(color: Colors.amber.withOpacity(0.2), blurRadius: 20, spreadRadius: 5)]
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.emoji_events, size: 50, color: Colors.amber),
const SizedBox(height: 10),
Text(loc.leaderboardTitle, style: getSharedTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))),
const SizedBox(height: 20),
SizedBox(
height: 350,
child: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('leaderboard').orderBy('xp', descending: true).limit(50).snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator(color: theme.playerBlue));
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return Center(child: Text("Ancora nessun campione...", style: TextStyle(color: theme.text.withOpacity(0.5))));
}
final rawDocs = snapshot.data!.docs;
final filteredDocs = rawDocs.where((doc) {
var data = doc.data() as Map<String, dynamic>;
String name = (data['name'] ?? '').toString().toUpperCase();
// Nascondiamo PIPPO dalla classifica
return name != 'PIPPO';
}).toList();
if (filteredDocs.isEmpty) {
return Center(child: Text("Ancora nessun campione...", style: TextStyle(color: theme.text.withOpacity(0.5))));
}
return ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: filteredDocs.length,
itemBuilder: (context, index) {
var doc = filteredDocs[index];
var data = doc.data() as Map<String, dynamic>;
String? myUid = FirebaseAuth.instance.currentUser?.uid;
bool isMe = doc.id == myUid;
String playerName = data['name'] ?? 'Unknown';
bool isOnline = false;
if (data['lastActive'] != null) {
Timestamp lastActive = data['lastActive'];
int diffInSeconds = DateTime.now().difference(lastActive.toDate()).inSeconds;
if (diffInSeconds.abs() < 180) isOnline = true;
}
return StatefulBuilder(
builder: (context, setStateItem) {
bool isFav = StorageService.instance.isFavorite(doc.id);
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: isMe ? theme.playerBlue.withOpacity(0.2) : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(10),
border: isMe ? Border.all(color: theme.playerBlue, width: 1.5) : null
),
child: Row(
children: [
Text("#${index + 1}", style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.w900, color: index == 0 ? Colors.amber : (index == 1 ? Colors.grey.shade400 : (index == 2 ? Colors.brown.shade300 : theme.text.withOpacity(0.5)))))),
const SizedBox(width: 15),
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
playerName,
style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: isMe ? FontWeight.w900 : FontWeight.bold, color: theme.text)),
overflow: TextOverflow.ellipsis,
)
),
if (isFav && !isMe && isOnline) ...[
const SizedBox(width: 8),
PulsingChallengeButton(
themeType: themeType,
onTap: () {
Navigator.pop(context);
if (onChallenge != null) {
onChallenge!(doc.id, playerName);
}
},
),
]
],
),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("Lv. ${data['level'] ?? 1}", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 12)),
Text("${data['xp'] ?? 0} XP", style: TextStyle(color: theme.text.withOpacity(0.6), fontSize: 10)),
],
),
if (!isMe) ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () async {
await StorageService.instance.toggleFavorite(doc.id, playerName);
setStateItem(() {});
},
child: Icon(isFav ? Icons.star : Icons.star_border, color: Colors.amber, size: 24),
)
]
],
),
);
}
);
}
);
}
),
),
const SizedBox(height: 15),
SizedBox(
width: double.infinity, height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.amber.shade700, foregroundColor: Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () => Navigator.pop(context),
child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
)
],
),
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
content = AnimatedCyberBorder(child: content);
}
return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: content);
}
}
// ===========================================================================
// 3. DIALOGO TUTORIAL
// ===========================================================================
class TutorialDialog extends StatelessWidget {
const TutorialDialog({super.key});
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
Color inkColor = const Color(0xFF111122);
String goldLabel = "ORO:";
String bombLabel = "BOMBA:";
String swapLabel = "SCAMBIO:";
String jokerLabel = "JOLLY:";
String iceLabel = "GHIACCIO:";
String multiplierLabel = "x2:";
String blockLabel = "BUCO NERO:";
if (themeType == AppThemeType.grimorio) {
goldLabel = "CORONA:";
bombLabel = "STREGA:";
jokerLabel = "GIULLARE:";
swapLabel = "TORNADO:";
multiplierLabel = "FULMINE:";
blockLabel = "METEORITE:";
} else if (themeType == AppThemeType.music) {
goldLabel = "DISCO D'ORO:";
bombLabel = "MUTO:";
jokerLabel = "DJ:";
swapLabel = "MIXER:";
iceLabel = "NOTA:";
multiplierLabel = "AVANTI VELOCE:";
blockLabel = "PAUSA:";
} else if (themeType == AppThemeType.arcade) {
goldLabel = "GETTONE:";
bombLabel = "FANTASMA:";
jokerLabel = "GAMEPAD:";
swapLabel = "SHUFFLE:";
blockLabel = "POWER OFF:";
} else if (themeType == AppThemeType.cyberpunk) {
goldLabel = "CHIP:";
bombLabel = "VIRUS:";
jokerLabel = "BOT:";
swapLabel = "NETWORK:";
blockLabel = "FIREWALL:";
} else if (themeType == AppThemeType.doodle) {
bombLabel = "VIRUS:";
}
Widget dialogContent = themeType == AppThemeType.doodle
? Transform.rotate(
angle: -0.01,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: Colors.yellow.shade50, strokeColor: inkColor, seed: 400),
child: Padding(
padding: const EdgeInsets.all(25.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(child: Text("COME GIOCARE", style: getSharedTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2)))),
const SizedBox(height: 20),
TutorialStep(icon: Icons.line_axis, text: "Chiudi i 4 lati di un quadrato per conquistare 1 punto e avere una mossa extra!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15),
TutorialStep(icon: Icons.lens_blur, text: "Ma presta attenzione! Ogni quadrato nasconde un'insidia o un regalo!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15),
const Divider(color: Colors.black26, thickness: 2),
const SizedBox(height: 10),
Center(child: Text("GLOSSARIO ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor)))),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.gold(themeType), iconColor: Colors.amber.shade700, text: "$goldLabel Chiudilo per ottenere +2 Punti.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.bomb(themeType), iconColor: Colors.deepPurple, text: "$bombLabel Non chiuderlo! Perderai -1 Punto.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.swap(themeType), iconColor: Colors.purpleAccent, text: "$swapLabel Inverte istantaneamente i punteggi dei giocatori.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.joker(themeType), iconColor: Colors.green.shade600, text: "$jokerLabel Scegli dove nasconderlo a inizio partita. Se lo chiudi tu +2, se lo chiude l'avversario -1!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.ice(themeType), iconColor: Colors.cyanAccent, text: "$iceLabel Devi cliccarlo due volte per poterlo rompere e chiudere.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.multiplier(themeType), iconColor: Colors.yellowAccent, text: "$multiplierLabel Non dà punti, ma raddoppia il punteggio della prossima casella che chiudi!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.block(themeType), iconColor: Colors.grey, text: "$blockLabel Questa casella non esiste. Se la chiudi perdi il turno.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 25),
Center(
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: Colors.red.shade200, strokeColor: inkColor, seed: 401),
child: Container(
height: 50, width: 150, alignment: Alignment.center,
child: Text("HO CAPITO!", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor))),
),
),
),
)
],
),
),
),
)
: Container(
padding: const EdgeInsets.all(25.0),
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
borderRadius: BorderRadius.circular(25),
border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5),
boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))],
),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(child: Text("COME GIOCARE", style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2)))),
const SizedBox(height: 20),
TutorialStep(icon: Icons.grid_4x4, text: "Chiudi i 4 lati di un quadrato per conquistare 1 punto e avere una mossa extra!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15),
TutorialStep(icon: Icons.lens_blur, text: "Ma presta attenzione! Ogni quadrato nasconde un'insidia o un regalo!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15),
const Divider(color: Colors.white24, thickness: 1.5),
const SizedBox(height: 10),
Center(child: Text("GLOSSARIO ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.7), letterSpacing: 1.5)))),
const SizedBox(height: 15),
TutorialStep(icon: ThemeIcons.gold(themeType), iconColor: Colors.amber, text: "$goldLabel Chiudilo per ottenere +2 Punti.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.bomb(themeType), iconColor: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.greenAccent : Colors.deepPurple, text: "$bombLabel Non chiuderlo! Perderai -1 Punto.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.swap(themeType), iconColor: Colors.purpleAccent, text: "$swapLabel Inverte istantaneamente i punteggi dei giocatori.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.joker(themeType), iconColor: theme.playerBlue, text: "$jokerLabel Scegli dove nasconderlo a inizio partita. Se lo chiudi tu +2, se lo chiude l'avversario -1!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.ice(themeType), iconColor: Colors.cyanAccent, text: "$iceLabel Devi cliccarlo due volte per poterlo rompere e chiudere.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.multiplier(themeType), iconColor: Colors.yellowAccent, text: "$multiplierLabel Non dà punti, ma raddoppia il punteggio della prossima casella che chiudi!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.block(themeType), iconColor: Colors.grey, text: "$blockLabel Questa casella non esiste. Se la chiudi perdi il turno.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 30),
SizedBox(
width: double.infinity, height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () => Navigator.pop(context),
child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
)
],
),
),
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
dialogContent = AnimatedCyberBorder(child: dialogContent);
}
return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), child: dialogContent);
}
}
class TutorialStep extends StatelessWidget {
final IconData icon;
final Color? iconColor;
final String text;
final AppThemeType themeType;
final Color inkColor;
final ThemeColors theme;
const TutorialStep({super.key, required this.icon, this.iconColor, required this.text, required this.themeType, required this.inkColor, required this.theme});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: iconColor ?? (themeType == AppThemeType.doodle ? inkColor : theme.playerBlue), size: 28),
const SizedBox(width: 15),
Expanded(
child: Text(text, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, color: themeType == AppThemeType.doodle ? inkColor : theme.text.withOpacity(0.8), height: 1.3))),
),
],
);
}
}
// ===========================================================================
// 4. WIDGET ANIMATO PER TASTO SFIDA
// ===========================================================================
class PulsingChallengeButton extends StatefulWidget {
final VoidCallback onTap;
final AppThemeType themeType;
const PulsingChallengeButton({super.key, required this.onTap, required this.themeType});
@override
State<PulsingChallengeButton> createState() => _PulsingChallengeButtonState();
}
class _PulsingChallengeButtonState extends State<PulsingChallengeButton> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 900))..repeat(reverse: true);
_animation = Tween<double>(begin: 0.3, end: 1.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final Color softGreen = Colors.green.shade400;
return GestureDetector(
onTap: widget.onTap,
child: FadeTransition(
opacity: _animation,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: softGreen.withOpacity(0.15),
border: Border.all(color: softGreen, width: 1.5),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.circle, color: softGreen, size: 8),
const SizedBox(width: 4),
Text(
"SFIDA",
style: getSharedTextStyle(widget.themeType, TextStyle(color: softGreen, fontSize: 10, fontWeight: FontWeight.bold))
),
],
),
),
),
);
}
}

View file

@ -1,820 +0,0 @@
// ===========================================================================
// FILE: lib/ui/home/home_modals.dart
// ===========================================================================
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
import '../../logic/game_controller.dart';
import '../../models/game_board.dart';
import '../../services/storage_service.dart';
import '../../services/multiplayer_service.dart';
import '../../l10n/app_localizations.dart';
import '../../widgets/painters.dart';
import '../../widgets/cyber_border.dart';
import '../game/game_screen.dart';
import '../multiplayer/lobby_widgets.dart';
class HomeModals {
static void showNameDialog(BuildContext context, VoidCallback onSuccess) {
final TextEditingController nameController = TextEditingController(text: StorageService.instance.playerName);
final TextEditingController passController = TextEditingController();
bool isLoadingAuth = false;
bool obscurePassword = true;
String errorMessage = "";
showDialog(
context: context,
barrierDismissible: false,
barrierColor: Colors.black.withOpacity(0.8),
builder: (dialogContext) {
final themeManager = dialogContext.watch<ThemeManager>();
final themeType = themeManager.currentThemeType;
Color inkColor = const Color(0xFF111122);
final loc = AppLocalizations.of(dialogContext)!;
return StatefulBuilder(
builder: (context, setStateDialog) {
Future<void> handleAuth(bool isLogin) async {
final name = nameController.text.trim();
final password = passController.text.trim();
setStateDialog(() { errorMessage = ""; isLoadingAuth = true; });
if (name.isEmpty || password.isEmpty) {
setStateDialog(() { errorMessage = "Inserisci Nome e Password!"; isLoadingAuth = false; });
return;
}
if (password.length < 6) {
setStateDialog(() { errorMessage = "La password deve avere almeno 6 caratteri!"; isLoadingAuth = false; });
return;
}
final fakeEmail = "${name.toLowerCase().replaceAll(' ', '')}@tetraq.game";
final currentUser = FirebaseAuth.instance.currentUser;
final ghostUid = (currentUser != null && currentUser.isAnonymous) ? currentUser.uid : null;
try {
if (isLogin) {
if (ghostUid != null) {
await FirebaseFirestore.instance.collection('leaderboard').doc(ghostUid).delete().catchError((e) => null);
}
await FirebaseAuth.instance.signInWithEmailAndPassword(email: fakeEmail, password: password);
final doc = await FirebaseFirestore.instance.collection('leaderboard').doc(FirebaseAuth.instance.currentUser!.uid).get();
if (doc.exists) {
final data = doc.data() as Map<String, dynamic>;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('totalXP', data['xp'] ?? 0);
await prefs.setInt('wins', data['wins'] ?? 0);
await prefs.setInt('losses', data['losses'] ?? 0);
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Bentornato $name! Dati sincronizzati."), backgroundColor: Colors.green));
} else {
StorageService.instance.syncLeaderboard();
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Bentornato $name! Profilo ripristinato."), backgroundColor: Colors.green));
}
} else {
if (currentUser != null && currentUser.isAnonymous) {
final credential = EmailAuthProvider.credential(email: fakeEmail, password: password);
await currentUser.linkWithCredential(credential);
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Profilo Cloud protetto con successo!"), backgroundColor: Colors.green));
} else {
await FirebaseAuth.instance.createUserWithEmailAndPassword(email: fakeEmail, password: password);
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Account creato con successo!"), backgroundColor: Colors.green));
}
}
await StorageService.instance.savePlayerName(name);
StorageService.instance.syncLeaderboard();
if (context.mounted) Navigator.of(dialogContext).pop();
onSuccess();
} on FirebaseAuthException catch (e) {
String msg = "Errore di autenticazione.";
if (e.code == 'email-already-in-use' || e.code == 'credential-already-in-use') {
msg = "Nome già registrato!\nSe sei tu, clicca su ACCEDI.";
} else if (e.code == 'user-not-found' || e.code == 'wrong-password' || e.code == 'invalid-credential') {
msg = "Nome o Password errati!";
if (isLogin && ghostUid != null) StorageService.instance.syncLeaderboard();
} else if (e.code == 'requires-recent-login') {
msg = "Errore di sessione. Riavvia l'app.";
}
setStateDialog(() { errorMessage = msg; isLoadingAuth = false; });
} catch (e) {
setStateDialog(() { errorMessage = "Errore imprevisto: $e"; isLoadingAuth = false; });
if (isLogin && ghostUid != null) StorageService.instance.syncLeaderboard();
}
}
Widget dialogContent = themeType == AppThemeType.doodle
? CustomPaint(
painter: DoodleBackgroundPainter(fillColor: Colors.yellow.shade100, strokeColor: inkColor, seed: 100),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(loc.welcomeTitle, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900, fontSize: 24, letterSpacing: 2.0)), textAlign: TextAlign.center),
const SizedBox(height: 10),
Text('Scegli una Password per il Cloud.\nI tuoi XP e il tuo Livello saranno protetti e non li perderai mai!', style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 13, fontWeight: FontWeight.bold)), textAlign: TextAlign.center),
const SizedBox(height: 15),
TextField(
controller: nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8,
style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 4)),
decoration: InputDecoration(
hintText: loc.nameHint, hintStyle: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.3), letterSpacing: 4)),
filled: false, counterText: "",
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: inkColor, width: 3)),
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.red.shade200, width: 5))
),
),
const SizedBox(height: 8),
TextField(
controller: passController, obscureText: obscurePassword, textAlign: TextAlign.center, maxLength: 20,
style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 8)),
decoration: InputDecoration(
hintText: "PASSWORD", hintStyle: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.3), letterSpacing: 4)),
filled: false, counterText: "",
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: inkColor, width: 3)),
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.red.shade200, width: 5)),
suffixIcon: IconButton(
icon: Icon(obscurePassword ? Icons.visibility : Icons.visibility_off, color: inkColor.withOpacity(0.6)),
onPressed: () { setStateDialog(() { obscurePassword = !obscurePassword; }); },
),
),
),
const SizedBox(height: 15),
if (errorMessage.isNotEmpty)
Padding(padding: const EdgeInsets.only(bottom: 10), child: Text(errorMessage, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.bold)), textAlign: TextAlign.center)),
Text("💡 Usa una password facile da ricordare!", style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.6), fontSize: 11, height: 1.3)), textAlign: TextAlign.center),
const SizedBox(height: 15),
isLoadingAuth ? CircularProgressIndicator(color: inkColor) : Row(
children: [
Expanded(child: GestureDetector(onTap: () => handleAuth(true), child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.blue.shade200, strokeColor: inkColor, seed: 101), child: Container(height: 45, alignment: Alignment.center, child: Text("ACCEDI", style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 14, fontWeight: FontWeight.bold, letterSpacing: 1.5))))))),
const SizedBox(width: 10),
Expanded(child: GestureDetector(onTap: () => handleAuth(false), child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.green.shade200, strokeColor: inkColor, seed: 102), child: Container(height: 45, alignment: Alignment.center, child: Text("REGISTRATI", style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 14, fontWeight: FontWeight.bold, letterSpacing: 1.5))))))),
],
),
],
),
),
),
)
: Container(
decoration: BoxDecoration(color: themeManager.currentColors.background, borderRadius: BorderRadius.circular(25), border: Border.all(color: themeManager.currentColors.playerBlue.withOpacity(0.5), width: 2), boxShadow: [BoxShadow(color: themeManager.currentColors.playerBlue.withOpacity(0.3), blurRadius: 20, spreadRadius: 5)]),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(loc.welcomeTitle, style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 1.5)), textAlign: TextAlign.center),
const SizedBox(height: 10),
Text('Scegli una Password per il Cloud.\nI tuoi XP e il tuo Livello saranno protetti e non li perderai mai!', style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.8), fontSize: 13, fontWeight: FontWeight.bold)), textAlign: TextAlign.center),
const SizedBox(height: 15),
TextField(
controller: nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8,
style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text, fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 4)),
decoration: InputDecoration(
hintText: loc.nameHint, hintStyle: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.3), letterSpacing: 4)),
filled: true, fillColor: themeManager.currentColors.text.withOpacity(0.05), counterText: "",
enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.gridLine.withOpacity(0.5), width: 2), borderRadius: BorderRadius.circular(15)),
focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.playerBlue, width: 3), borderRadius: BorderRadius.circular(15))
),
),
const SizedBox(height: 10),
TextField(
controller: passController, obscureText: obscurePassword, textAlign: TextAlign.center, maxLength: 20,
style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text, fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 8)),
decoration: InputDecoration(
hintText: "PASSWORD", hintStyle: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.3), letterSpacing: 4)),
filled: true, fillColor: themeManager.currentColors.text.withOpacity(0.05), counterText: "",
enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.gridLine.withOpacity(0.5), width: 2), borderRadius: BorderRadius.circular(15)),
focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.playerBlue, width: 3), borderRadius: BorderRadius.circular(15)),
suffixIcon: IconButton(
icon: Icon(obscurePassword ? Icons.visibility : Icons.visibility_off, color: themeManager.currentColors.text.withOpacity(0.6)),
onPressed: () { setStateDialog(() { obscurePassword = !obscurePassword; }); },
),
),
),
const SizedBox(height: 15),
if (errorMessage.isNotEmpty)
Padding(padding: const EdgeInsets.only(bottom: 10), child: Text(errorMessage, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.redAccent, fontSize: 14, fontWeight: FontWeight.bold)), textAlign: TextAlign.center)),
Text("💡 Usa una password facile da ricordare!", style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.6), fontSize: 11, height: 1.3)), textAlign: TextAlign.center),
const SizedBox(height: 20),
isLoadingAuth ? CircularProgressIndicator(color: themeManager.currentColors.playerBlue) : Row(
children: [
Expanded(child: SizedBox(height: 45, child: ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: themeManager.currentColors.text.withOpacity(0.1), foregroundColor: themeManager.currentColors.text, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), side: BorderSide(color: themeManager.currentColors.playerBlue, width: 1.5)), onPressed: () => handleAuth(true), child: Text("ACCEDI", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, letterSpacing: 1.0)))))),
const SizedBox(width: 10),
Expanded(child: SizedBox(height: 45, child: ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: themeManager.currentColors.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () => handleAuth(false), child: Text("REGISTRATI", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, letterSpacing: 1.0)))))),
],
),
],
),
),
),
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) dialogContent = AnimatedCyberBorder(child: dialogContent);
return PopScope(
canPop: false,
child: Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: dialogContent)
);
},
);
},
);
}
static Widget _buildTimeOption(String label, String sub, String value, String current, ThemeColors theme, AppThemeType type, VoidCallback onTap) {
bool isSel = value == current;
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
height: 50,
decoration: BoxDecoration(
color: isSel ? Colors.orange.shade600 : (type == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05)),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: isSel ? Colors.orange.shade800 : (type == AppThemeType.doodle ? const Color(0xFF111122) : Colors.white24), width: isSel ? 2 : 1.5),
boxShadow: isSel && type != AppThemeType.doodle ? [BoxShadow(color: Colors.orange.withOpacity(0.5), blurRadius: 8)] : [],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(label, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white : (type == AppThemeType.doodle ? const Color(0xFF111122) : theme.text), fontWeight: FontWeight.w900, fontSize: 13))),
if (sub.isNotEmpty) Text(sub, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white70 : (type == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.5)), fontWeight: FontWeight.bold, fontSize: 8))),
],
),
),
),
);
}
static void showChallengeSetupDialog(BuildContext context, String targetName, Function(int radius, ArenaShape shape, String timeMode) onStart) {
int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed';
bool isChaosUnlocked = StorageService.instance.playerLevel >= 7;
showDialog(
context: context, barrierColor: Colors.black.withOpacity(0.8),
builder: (ctx) {
final themeManager = ctx.watch<ThemeManager>();
final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType;
Color inkColor = const Color(0xFF111122);
return StatefulBuilder(
builder: (context, setStateDialog) {
Widget dialogContent = themeType == AppThemeType.doodle
? Transform.rotate(
angle: 0.015,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 200),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(25.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("SFIDA $targetName", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 2))),
const SizedBox(height: 10),
Text("IMPOSTAZIONI STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))),
const SizedBox(height: 25),
Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15),
Wrap(
spacing: 12, runSpacing: 12, alignment: WrapAlignment.center,
children: [
NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)),
NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)),
NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)),
NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)),
NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)),
],
),
const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20),
Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)),
NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)),
NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)),
NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)),
],
),
const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20),
Text("TEMPO E OPZIONI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10),
Row(
children: [
_buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')),
_buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')),
_buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')),
],
),
const SizedBox(height: 35),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
Navigator.pop(ctx);
onStart(localRadius, localShape, localTimeMode);
},
child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: theme.playerRed, strokeColor: inkColor, seed: 300), child: Container(height: 55, alignment: Alignment.center, child: Text("AVVIA", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white))))),
),
),
const SizedBox(width: 15),
Expanded(
child: GestureDetector(
onTap: () => Navigator.pop(ctx),
child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.grey.shade400, strokeColor: inkColor, seed: 301), child: Container(height: 55, alignment: Alignment.center, child: Text("ANNULLA", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white))))),
),
),
],
)
],
),
),
),
),
)
: Container(
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
borderRadius: BorderRadius.circular(25), border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5),
boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))],
),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("SFIDA $targetName", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 2))),
const SizedBox(height: 10),
Text("IMPOSTAZIONI STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))),
const SizedBox(height: 20),
Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
Wrap(
spacing: 10, runSpacing: 10, alignment: WrapAlignment.center,
children: [
NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)),
NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)),
NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)),
NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)),
NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)),
],
),
const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20),
Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)),
NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)),
NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)),
NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)),
],
),
const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20),
Text("TEMPO E OPZIONI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
Row(
children: [
_buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')),
_buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')),
_buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')),
],
),
const SizedBox(height: 30),
Row(
children: [
Expanded(
child: SizedBox(
height: 55,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () {
Navigator.pop(ctx);
onStart(localRadius, localShape, localTimeMode);
},
child: const Text("AVVIA", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
),
),
const SizedBox(width: 15),
Expanded(
child: SizedBox(
height: 55,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey.shade800, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () => Navigator.pop(ctx),
child: const Text("ANNULLA", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
),
),
],
)
],
),
),
),
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
dialogContent = AnimatedCyberBorder(child: dialogContent);
}
return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), child: dialogContent);
},
);
}
);
}
static void showMatchSetupDialog(BuildContext context, bool isVsCPU) {
int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed';
bool isChaosUnlocked = StorageService.instance.playerLevel >= 7;
final loc = AppLocalizations.of(context)!;
showDialog(
context: context, barrierColor: Colors.black.withOpacity(0.8),
builder: (ctx) {
final themeManager = ctx.watch<ThemeManager>();
final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType;
Color inkColor = const Color(0xFF111122);
return StatefulBuilder(
builder: (context, setStateDialog) {
Widget dialogContent = themeType == AppThemeType.doodle
? Transform.rotate(
angle: 0.015,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 200),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(25.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(children: [ SizedBox(width: 40, child: IconButton(padding: EdgeInsets.zero, alignment: Alignment.centerLeft, icon: Icon(Icons.arrow_back_ios_new, color: inkColor, size: 26), onPressed: () => Navigator.pop(ctx))), Expanded(child: Text(isVsCPU ? loc.cpuTitle : loc.localTitle, textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2)))), const SizedBox(width: 40) ]),
const SizedBox(height: 25),
if (isVsCPU) ...[
Icon(Icons.smart_toy, size: 50, color: inkColor.withOpacity(0.6)), const SizedBox(height: 10),
Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: inkColor))), const SizedBox(height: 10),
Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: inkColor.withOpacity(0.8), height: 1.4))), const SizedBox(height: 25),
Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20),
] else ...[
Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15),
Wrap(
spacing: 12, runSpacing: 12, alignment: WrapAlignment.center,
children: [
NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)),
NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)),
NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)),
NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)),
NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)),
],
),
const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20),
Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)),
NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)),
NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)),
NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)),
],
),
const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20),
Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10),
Row(
children: [
_buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')),
_buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')),
_buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')),
],
), const SizedBox(height: 35),
],
Transform.rotate(
angle: -0.02,
child: GestureDetector(
onTap: () { Navigator.pop(ctx); context.read<GameController>().startNewGame(localRadius, vsCPU: isVsCPU, shape: localShape, timeMode: localTimeMode); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); },
child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.green.shade200, strokeColor: inkColor, seed: 300), child: Container(height: 65, width: double.infinity, alignment: Alignment.center, child: Text(loc.startGame, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: inkColor))))),
),
)
],
),
),
),
),
)
: Container(
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
borderRadius: BorderRadius.circular(25), border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5),
boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))],
),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(children: [ SizedBox(width: 40, child: IconButton(padding: EdgeInsets.zero, alignment: Alignment.centerLeft, icon: Icon(Icons.arrow_back_ios_new, color: theme.text, size: 26), onPressed: () => Navigator.pop(ctx))), Expanded(child: Text(isVsCPU ? loc.cpuTitle : loc.localTitle, textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2)))), const SizedBox(width: 40) ]),
const SizedBox(height: 20),
if (isVsCPU) ...[
Icon(Icons.smart_toy, size: 50, color: theme.playerBlue), const SizedBox(height: 10),
Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), const SizedBox(height: 10),
Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: theme.text.withOpacity(0.7), height: 1.4))), const SizedBox(height: 20),
Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20),
] else ...[
Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
Wrap(
spacing: 10, runSpacing: 10, alignment: WrapAlignment.center,
children: [
NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)),
NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)),
NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)),
NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)),
NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)),
],
),
const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20),
Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)),
NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)),
NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)),
NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)),
],
),
const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20),
Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
Row(
children: [
_buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')),
_buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')),
_buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')),
],
), const SizedBox(height: 30),
],
SizedBox(
width: double.infinity, height: 60,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: isVsCPU ? Colors.purple.shade400 : theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
onPressed: () { Navigator.pop(ctx); context.read<GameController>().startNewGame(localRadius, vsCPU: isVsCPU, shape: localShape, timeMode: localTimeMode); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); },
child: Text(loc.startGame, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
)
],
),
),
),
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
dialogContent = AnimatedCyberBorder(child: dialogContent);
}
return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), child: dialogContent);
},
);
}
);
}
static void showWaitingDialog({
required BuildContext context,
required String code,
required bool isPublicRoom,
required int selectedRadius,
required ArenaShape selectedShape,
required String selectedTimeMode,
required MultiplayerService multiplayerService,
required VoidCallback onRoomStarted,
required VoidCallback onCleanup,
}) {
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
final theme = dialogContext.watch<ThemeManager>().currentColors;
final themeType = dialogContext.read<ThemeManager>().currentThemeType;
Widget dialogContent = Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: theme.playerRed), const SizedBox(height: 25),
Text("CODICE STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.6), letterSpacing: 2))),
Text(code, style: getSharedTextStyle(themeType, TextStyle(fontSize: 40, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 8, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: theme.playerRed.withOpacity(0.5), blurRadius: 10)]))),
const SizedBox(height: 25),
Transform.rotate(
angle: themeType == AppThemeType.doodle ? 0.02 : 0,
child: Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: themeType == AppThemeType.doodle ? theme.text : theme.playerBlue.withOpacity(0.3), width: themeType == AppThemeType.doodle ? 2 : 1.5),
boxShadow: themeType == AppThemeType.doodle
? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(4, 4))]
: [BoxShadow(color: theme.playerBlue.withOpacity(0.1), blurRadius: 10)]
),
child: Column(
children: [
Icon(isPublicRoom ? Icons.podcasts : Icons.share, color: theme.playerBlue, size: 32), const SizedBox(height: 12),
Text(isPublicRoom ? "Sei in Bacheca!" : "Invito inviato", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))),
const SizedBox(height: 8),
Text(isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Attendi che il tuo amico accetti la sfida. Non chiudere questa finestra.", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))),
],
),
),
),
],
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
dialogContent = AnimatedCyberBorder(child: dialogContent);
} else {
dialogContent = Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white.withOpacity(0.95) : theme.background,
borderRadius: BorderRadius.circular(25),
border: Border.all(color: themeType == AppThemeType.doodle ? theme.text : theme.gridLine.withOpacity(0.5), width: 2),
boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: theme.text.withOpacity(0.6), offset: const Offset(8, 8))] : []
),
child: dialogContent
);
}
return StreamBuilder<DocumentSnapshot>(
stream: multiplayerService.listenToRoom(code),
builder: (ctx, snapshot) {
if (snapshot.hasData && snapshot.data!.exists) {
var data = snapshot.data!.data() as Map<String, dynamic>;
if (data['status'] == 'playing') {
onRoomStarted();
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pop(ctx);
context.read<GameController>().startNewGame(selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: selectedShape, timeMode: selectedTimeMode);
Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen()));
});
}
}
return PopScope(
canPop: false,
onPopInvoked: (didPop) {
if (didPop) return;
onCleanup();
Navigator.pop(ctx);
},
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
dialogContent,
const SizedBox(height: 20),
TextButton(
onPressed: () {
onCleanup();
Navigator.pop(ctx);
},
child: Text("ANNULLA", style: getSharedTextStyle(themeType, TextStyle(color: Colors.red, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 2.0, shadows: themeType == AppThemeType.doodle ? [] : [const Shadow(color: Colors.black, blurRadius: 2)]))),
),
],
),
),
);
},
);
}
);
}
static void showJoinPromptDialog(BuildContext context, String roomCode, Function(String) onConfirm) {
showDialog(
context: context,
builder: (context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
return AlertDialog(
backgroundColor: themeType == AppThemeType.doodle ? Colors.white : theme.background,
shape: themeType == AppThemeType.doodle ? RoundedRectangleBorder(borderRadius: BorderRadius.circular(15), side: BorderSide(color: theme.text, width: 2)) : null,
title: Text("Invito Trovato!", style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold))),
content: Text("Vuoi unirti alla stanza $roomCode?", style: getSharedTextStyle(themeType, TextStyle(color: theme.text))),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text("No", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.red)))),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: themeType == AppThemeType.doodle ? Colors.transparent : theme.playerBlue, elevation: 0, side: themeType == AppThemeType.doodle ? BorderSide(color: theme.text, width: 1.5) : BorderSide.none),
onPressed: () {
Navigator.of(context).pop();
onConfirm(roomCode);
},
child: Text(AppLocalizations.of(context)!.joinMatch, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : Colors.white, fontWeight: FontWeight.bold))),
),
],
);
}
);
}
static void showFavoritesDialog(BuildContext context, Function(String, String) onInvite) {
final favs = StorageService.instance.favorites;
showDialog(
context: context,
builder: (ctx) {
final themeManager = ctx.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
return AlertDialog(
backgroundColor: theme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text("I TUOI PREFERITI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold))),
content: Container(
width: double.maxFinite,
height: 300,
decoration: BoxDecoration(
border: Border.all(color: theme.playerRed, width: 2),
borderRadius: BorderRadius.circular(10)
),
child: favs.isEmpty
? Center(child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text("Non hai ancora aggiunto nessun preferito dalla Classifica!", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6)))),
))
: ListView.builder(
itemCount: favs.length,
itemBuilder: (c, i) {
return ListTile(
title: Text(favs[i]['name']!, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontSize: 18, fontWeight: FontWeight.bold))),
trailing: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
onPressed: () {
Navigator.pop(ctx);
onInvite(favs[i]['uid']!, favs[i]['name']!);
},
child: Text("SFIDA", style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
),
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text("CHIUDI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))),
),
],
);
}
);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,558 +0,0 @@
// ===========================================================================
// FILE: lib/ui/multiplayer/lobby_widgets.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:google_fonts/google_fonts.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
TextStyle getLobbyTextStyle(AppThemeType themeType, TextStyle baseStyle) {
if (themeType == AppThemeType.doodle) {
return GoogleFonts.permanentMarker(textStyle: baseStyle);
} else if (themeType == AppThemeType.arcade) {
return GoogleFonts.pressStart2p(textStyle: baseStyle.copyWith(
fontSize: baseStyle.fontSize != null ? baseStyle.fontSize! * 0.75 : null,
letterSpacing: 0.5,
));
} else if (themeType == AppThemeType.grimorio) {
return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold));
} else if (themeType == AppThemeType.music) {
return GoogleFonts.audiowide(textStyle: baseStyle.copyWith(letterSpacing: 1.5));
}
return baseStyle;
}
class NeonShapeButton extends StatelessWidget {
final IconData icon;
final String label;
final bool isSelected;
final ThemeColors theme;
final AppThemeType themeType;
final VoidCallback onTap;
final bool isLocked;
final bool isSpecial;
const NeonShapeButton({
super.key, required this.icon, required this.label, required this.isSelected,
required this.theme, required this.themeType, required this.onTap,
this.isLocked = false, this.isSpecial = false
});
Color _getDoodleColor() {
switch (label) {
case 'Rombo': return Colors.blue.shade700;
case 'Croce': return Colors.teal.shade700;
case 'Buco': return Colors.pink.shade600;
case 'Clessidra': return Colors.deepPurple.shade600;
case 'Caos': return Colors.blueGrey.shade800;
default: return Colors.blue.shade700;
}
}
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
Color doodleColor = isLocked ? Colors.grey : _getDoodleColor();
double tilt = (label.length % 2 == 0) ? -0.03 : 0.04;
return Transform.rotate(
angle: tilt,
child: GestureDetector(
onTap: isLocked ? null : onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
transform: Matrix4.translationValues(0, isSelected ? 3 : 0, 0),
decoration: BoxDecoration(
color: isSelected ? doodleColor : Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15), topRight: Radius.circular(8),
bottomLeft: Radius.circular(6), bottomRight: Radius.circular(18),
),
border: Border.all(color: isSelected ? theme.text : doodleColor.withOpacity(0.5), width: isSelected ? 2.5 : 1.5),
boxShadow: isSelected
? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(3, 4), blurRadius: 0)]
: [BoxShadow(color: doodleColor.withOpacity(0.2), offset: const Offset(2, 2), blurRadius: 0)],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(isLocked ? Icons.lock : icon, color: isSelected ? Colors.white : doodleColor, size: 20),
const SizedBox(height: 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)))),
],
),
),
),
);
}
Color mainColor = isSpecial && !isLocked ? Colors.purpleAccent : theme.playerBlue;
return GestureDetector(
onTap: isLocked ? null : onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
transform: Matrix4.translationValues(0, isSelected ? 2 : 0, 0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isLocked
? [Colors.grey.withOpacity(0.1), Colors.black.withOpacity(0.2)]
: isSelected
? [mainColor.withOpacity(0.3), mainColor.withOpacity(0.1)]
: [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)],
),
border: Border.all(
color: isLocked ? Colors.transparent : (isSelected ? mainColor : Colors.white.withOpacity(0.1)),
width: isSelected ? 2 : 1,
),
boxShadow: isLocked ? [] : isSelected
? [BoxShadow(color: mainColor.withOpacity(0.5), blurRadius: 15, spreadRadius: 1, offset: const Offset(0, 0))]
: [
BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)),
BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1)),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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),
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)))),
],
),
),
);
}
}
class NeonSizeButton extends StatelessWidget {
final String label;
final bool isSelected;
final ThemeColors theme;
final AppThemeType themeType;
final VoidCallback onTap;
const NeonSizeButton({super.key, required this.label, required this.isSelected, required this.theme, required this.themeType, required this.onTap});
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
Color doodleColor = label == 'MAX' ? Colors.red.shade700 : Colors.blueGrey.shade600;
double tilt = (label == 'M' || label == 'MAX') ? 0.05 : -0.04;
return Transform.rotate(
angle: tilt,
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 42, height: 40,
transform: Matrix4.translationValues(0, isSelected ? 3 : 0, 0),
decoration: BoxDecoration(
color: isSelected ? doodleColor : Colors.white,
borderRadius: const BorderRadius.all(Radius.elliptical(25, 20)),
border: Border.all(color: isSelected ? theme.text : doodleColor.withOpacity(0.5), width: 2),
boxShadow: isSelected
? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(3, 4), blurRadius: 0)]
: [BoxShadow(color: doodleColor.withOpacity(0.2), offset: const Offset(2, 2), blurRadius: 0)],
),
child: Center(
child: FittedBox(fit: BoxFit.scaleDown, child: Text(label, style: getLobbyTextStyle(themeType, TextStyle(color: isSelected ? Colors.white : doodleColor, fontSize: 13, fontWeight: FontWeight.w900)))),
),
),
),
);
}
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
width: 42, height: 42,
transform: Matrix4.translationValues(0, isSelected ? 2 : 0, 0),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isSelected
? [theme.playerRed.withOpacity(0.3), theme.playerRed.withOpacity(0.1)]
: [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)],
),
border: Border.all(color: isSelected ? theme.playerRed : Colors.white.withOpacity(0.1), width: isSelected ? 2 : 1),
boxShadow: isSelected
? [BoxShadow(color: theme.playerRed.withOpacity(0.5), blurRadius: 15, spreadRadius: 1)]
: [
BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)),
BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1)),
],
),
child: Center(
child: FittedBox(fit: BoxFit.scaleDown, child: Text(label, style: getLobbyTextStyle(themeType, TextStyle(color: isSelected ? Colors.white : theme.text.withOpacity(0.6), fontSize: 12, fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold)))),
),
),
);
}
}
class NeonTimeSwitch extends StatelessWidget {
final bool isTimeMode;
final ThemeColors theme;
final AppThemeType themeType;
final VoidCallback onTap;
const NeonTimeSwitch({super.key, required this.isTimeMode, required this.theme, required this.themeType, required this.onTap});
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
Color doodleColor = Colors.orange.shade700;
return Transform.rotate(
angle: -0.015,
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
transform: Matrix4.translationValues(0, isTimeMode ? 3 : 0, 0),
decoration: BoxDecoration(
color: isTimeMode ? doodleColor : Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8), topRight: Radius.circular(15),
bottomLeft: Radius.circular(15), bottomRight: Radius.circular(6),
),
border: Border.all(color: isTimeMode ? theme.text : doodleColor.withOpacity(0.5), width: 2.5),
boxShadow: isTimeMode
? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(4, 5), blurRadius: 0)]
: [BoxShadow(color: doodleColor.withOpacity(0.2), offset: const Offset(2, 2), blurRadius: 0)],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isTimeMode ? Icons.timer : Icons.timer_off, color: isTimeMode ? Colors.white : doodleColor, size: 20),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isTimeMode ? 'A TEMPO' : 'RELAX', style: getLobbyTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : doodleColor, fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 1.0)))),
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isTimeMode ? '15s a mossa' : 'Senza limiti', style: getLobbyTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : doodleColor.withOpacity(0.8), fontSize: 9, fontWeight: FontWeight.bold)))),
],
),
),
],
),
),
),
);
}
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isTimeMode
? [Colors.amber.withOpacity(0.25), Colors.amber.withOpacity(0.05)]
: [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)],
),
border: Border.all(color: isTimeMode ? Colors.amber : Colors.white.withOpacity(0.1), width: isTimeMode ? 2 : 1),
boxShadow: isTimeMode
? [BoxShadow(color: Colors.amber.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)]
: [
BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)),
BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1)),
],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isTimeMode ? Icons.timer : Icons.timer_off, color: isTimeMode ? Colors.amber : theme.text.withOpacity(0.5), size: 20),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isTimeMode ? 'A TEMPO' : 'RELAX', style: getLobbyTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : theme.text.withOpacity(0.5), fontWeight: FontWeight.w900, fontSize: 11, letterSpacing: 1.5)))),
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isTimeMode ? '15s a mossa' : 'Senza limiti', style: getLobbyTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.amber.shade200 : theme.text.withOpacity(0.4), fontSize: 9, fontWeight: FontWeight.bold)))),
],
),
),
],
),
),
);
}
}
class NeonPrivacySwitch extends StatelessWidget {
final bool isPublic;
final ThemeColors theme;
final AppThemeType themeType;
final VoidCallback onTap;
const NeonPrivacySwitch({super.key, required this.isPublic, required this.theme, required this.themeType, required this.onTap});
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
Color doodleColor = isPublic ? Colors.green.shade600 : Colors.red.shade600;
return Transform.rotate(
angle: 0.015,
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
transform: Matrix4.translationValues(0, isPublic ? 3 : 0, 0),
decoration: BoxDecoration(
color: isPublic ? doodleColor : Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15), topRight: Radius.circular(8),
bottomLeft: Radius.circular(6), bottomRight: Radius.circular(15),
),
border: Border.all(color: isPublic ? theme.text : doodleColor.withOpacity(0.5), width: 2.5),
boxShadow: [BoxShadow(color: isPublic ? theme.text.withOpacity(0.8) : doodleColor.withOpacity(0.2), offset: const Offset(4, 5), blurRadius: 0)],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.white : doodleColor, size: 20),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isPublic ? 'STANZA PUBBLICA' : 'STANZA PRIVATA', style: getLobbyTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : doodleColor, 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.white : doodleColor.withOpacity(0.8), fontSize: 9, fontWeight: FontWeight.bold)))),
],
),
),
],
),
),
),
);
}
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isPublic
? [Colors.greenAccent.withOpacity(0.25), Colors.greenAccent.withOpacity(0.05)]
: [theme.playerRed.withOpacity(0.25), theme.playerRed.withOpacity(0.05)],
),
border: Border.all(color: isPublic ? Colors.greenAccent : theme.playerRed, width: isPublic ? 2 : 1),
boxShadow: isPublic
? [BoxShadow(color: Colors.greenAccent.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)]
: [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4))],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.greenAccent : theme.playerRed, size: 20),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
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 ? '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)))),
],
),
),
],
),
),
);
}
}
class NeonInviteFavoriteButton extends StatelessWidget {
final ThemeColors theme;
final AppThemeType themeType;
final VoidCallback onTap;
const NeonInviteFavoriteButton({super.key, required this.theme, required this.themeType, required this.onTap});
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
Color doodleColor = Colors.pink.shade600;
return Transform.rotate(
angle: -0.015,
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8), topRight: Radius.circular(15),
bottomLeft: Radius.circular(15), bottomRight: Radius.circular(6),
),
border: Border.all(color: doodleColor.withOpacity(0.5), width: 2.5),
boxShadow: [BoxShadow(color: doodleColor.withOpacity(0.2), offset: const Offset(4, 5), blurRadius: 0)],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite, color: doodleColor, size: 20),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('PREFERITI', style: getLobbyTextStyle(themeType, TextStyle(color: doodleColor, fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 1.0)))),
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('Invita amico', style: getLobbyTextStyle(themeType, TextStyle(color: doodleColor.withOpacity(0.8), fontSize: 9, fontWeight: FontWeight.bold)))),
],
),
),
],
),
),
),
);
}
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.pinkAccent.withOpacity(0.25), Colors.pinkAccent.withOpacity(0.05)],
),
border: Border.all(color: Colors.pinkAccent, width: 1.5),
boxShadow: [BoxShadow(color: Colors.pinkAccent.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.favorite, color: Colors.pinkAccent, size: 20),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('PREFERITI', style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 11, letterSpacing: 1.5)))),
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('Invita amico', style: getLobbyTextStyle(themeType, TextStyle(color: Colors.pinkAccent.shade200, fontSize: 9, fontWeight: FontWeight.bold)))),
],
),
),
],
),
),
);
}
}
class NeonActionButton extends StatelessWidget {
final String label;
final Color color;
final VoidCallback onTap;
final ThemeColors theme;
final AppThemeType themeType;
const NeonActionButton({super.key, required this.label, required this.color, required this.onTap, required this.theme, required this.themeType});
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
double tilt = (label == "UNISCITI" || label == "ANNULLA") ? -0.015 : 0.02;
return Transform.rotate(
angle: tilt,
child: GestureDetector(
onTap: onTap,
child: Container(
height: 50,
decoration: BoxDecoration(
color: color,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10), topRight: Radius.circular(20),
bottomLeft: Radius.circular(25), bottomRight: Radius.circular(10),
),
border: Border.all(color: theme.text, width: 3.0),
boxShadow: [BoxShadow(color: theme.text.withOpacity(0.9), offset: const Offset(4, 4), blurRadius: 0)],
),
child: Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Text(label, style: getLobbyTextStyle(themeType, const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: Colors.white))),
),
),
),
),
),
);
}
return GestureDetector(
onTap: onTap,
child: Container(
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [color.withOpacity(0.9), color.withOpacity(0.6)]),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.white.withOpacity(0.3), width: 1.5),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.5), offset: const Offset(4, 8), blurRadius: 12),
BoxShadow(color: color.withOpacity(0.3), offset: const Offset(0, 0), blurRadius: 15, spreadRadius: 1),
],
),
child: Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Text(label, style: getLobbyTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white, shadows: [Shadow(color: Colors.black, blurRadius: 2, offset: Offset(1, 1))]))),
),
),
),
),
);
}
}

View file

@ -1,473 +0,0 @@
// ===========================================================================
// FILE: lib/ui/profile/profile_screen.dart
// ===========================================================================
import 'dart:ui';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
import '../../services/storage_service.dart';
import '../../widgets/painters.dart';
import '../../widgets/cyber_border.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _passController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;
String _errorMessage = "";
List<String> _nameSuggestions = [];
bool _isGhostMode = false;
late User _currentUser;
@override
void initState() {
super.initState();
_currentUser = FirebaseAuth.instance.currentUser!;
_loadGhostMode();
}
@override
void dispose() {
_nameController.dispose();
_passController.dispose();
super.dispose();
}
Future<void> _loadGhostMode() async {
try {
var doc = await FirebaseFirestore.instance.collection('leaderboard').doc(_currentUser.uid).get();
if (doc.exists && doc.data()!.containsKey('isGhost')) {
setState(() {
_isGhostMode = doc.data()!['isGhost'];
});
}
} catch (e) {
debugPrint("Errore caricamento Ghost Mode: $e");
}
}
Future<void> _toggleGhostMode(bool value) async {
setState(() => _isGhostMode = value);
try {
await FirebaseFirestore.instance.collection('leaderboard').doc(_currentUser.uid).set(
{'isGhost': value}, SetOptions(merge: true)
);
} catch (e) {
debugPrint("Errore salvataggio Ghost Mode: $e");
}
}
String _getPlayerTitle(int level) {
if (level < 10) return "Principiante";
if (level < 20) return "Apprendista";
if (level < 40) return "Sfidante";
if (level < 60) return "Tattico dell'Arena";
if (level < 80) return "Maestro dei Quadrati";
if (level < 100) return "Gran Maestro";
if (level < 130) return "Campione della Griglia";
if (level < 160) return "Entità Digitale";
if (level < 200) return "Oracolo del Codice";
return "Leggenda Suprema";
}
Future<void> _handleRegistration() async {
final name = _nameController.text.trim().toUpperCase();
final password = _passController.text.trim();
setState(() { _errorMessage = ""; _nameSuggestions.clear(); _isLoading = true; });
if (name.isEmpty || password.isEmpty) {
setState(() { _errorMessage = "Compila tutti i campi!"; _isLoading = false; });
return;
}
if (password.length < 6) {
setState(() { _errorMessage = "Password troppo corta (min. 6 caratteri)"; _isLoading = false; });
return;
}
try {
// 1. Controllo univocità del nome
var existingUser = await FirebaseFirestore.instance.collection('leaderboard').where('name', isEqualTo: name).get();
if (existingUser.docs.isNotEmpty && existingUser.docs.first.id != _currentUser.uid) {
// Nome già preso, generiamo suggerimenti
List<String> suggestions = [];
int attempts = 0;
final rand = Random();
while(suggestions.length < 3 && attempts < 15) {
String candidate = "$name${rand.nextInt(99) + 1}";
var check = await FirebaseFirestore.instance.collection('leaderboard').where('name', isEqualTo: candidate).get();
if (check.docs.isEmpty && !suggestions.contains(candidate)) {
suggestions.add(candidate);
}
attempts++;
}
setState(() {
_errorMessage = "Nome già in uso! Scegline un altro:";
_nameSuggestions = suggestions;
_isLoading = false;
});
return;
}
// 2. Registrazione sicura
final fakeEmail = "${name.replaceAll(' ', '')}@tetraq.game".toLowerCase();
if (_currentUser.isAnonymous) {
final credential = EmailAuthProvider.credential(email: fakeEmail, password: password);
await _currentUser.linkWithCredential(credential);
}
await StorageService.instance.savePlayerName(name);
await StorageService.instance.syncLeaderboard();
setState(() { _isLoading = false; });
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Account Protetto con Successo!"), backgroundColor: Colors.green));
} on FirebaseAuthException catch (e) {
String msg = "Errore di connessione.";
if (e.code == 'email-already-in-use' || e.code == 'credential-already-in-use') msg = "Utente già registrato. Se sei tu, fai il login.";
setState(() { _errorMessage = msg; _isLoading = false; });
} catch (e) {
setState(() { _errorMessage = "Errore: $e"; _isLoading = false; });
}
}
Future<void> _deleteAccount() async {
bool confirm = await showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.black87,
title: const Text("ATTENZIONE", style: TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold)),
content: const Text("Stai per eliminare definitivamente il tuo profilo, i tuoi XP e le statistiche.\nL'operazione è irreversibile.\n\nVuoi procedere?", style: TextStyle(color: Colors.white)),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text("ANNULLA", style: TextStyle(color: Colors.grey))),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
onPressed: () => Navigator.pop(ctx, true),
child: const Text("SÌ, ELIMINA", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
],
)
) ?? false;
if (!confirm) return;
setState(() => _isLoading = true);
try {
// 1. Elimina record da Firestore
await FirebaseFirestore.instance.collection('leaderboard').doc(_currentUser.uid).delete();
// 2. Elimina l'utente Auth
await _currentUser.delete();
// 3. Pulisci i dati locali sensibili
final prefs = await SharedPreferences.getInstance();
await prefs.remove('totalXP');
await prefs.remove('wins');
await prefs.remove('losses');
await prefs.remove('cpuLevel');
await prefs.remove('playerName');
await prefs.remove('favorites');
// 4. Ricrea un anonimo pulito e torna alla Home
await FirebaseAuth.instance.signInAnonymously();
await StorageService.instance.init();
if (mounted) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
} on FirebaseAuthException catch (e) {
setState(() => _isLoading = false);
if (e.code == 'requires-recent-login') {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Per sicurezza, riavvia l'app prima di eliminare l'account."), backgroundColor: Colors.redAccent));
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore: ${e.message}"), backgroundColor: Colors.redAccent));
}
}
}
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
Color inkColor = const Color(0xFF111122);
int wins = StorageService.instance.wins;
int losses = StorageService.instance.losses;
int totalGames = wins + losses;
double winRate = totalGames > 0 ? (wins / totalGames) * 100 : 0.0;
int level = StorageService.instance.playerLevel;
String title = _getPlayerTitle(level);
String playerName = StorageService.instance.playerName;
if (playerName.isEmpty) playerName = "GUEST";
bool isAnon = _currentUser.isAnonymous;
return Scaffold(
backgroundColor: theme.background,
appBar: AppBar(
title: Text("PROFILO GIOCATORE", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontWeight: FontWeight.w900, letterSpacing: 1.5))),
backgroundColor: Colors.transparent,
elevation: 0,
iconTheme: IconThemeData(color: themeType == AppThemeType.doodle ? inkColor : theme.text),
),
body: Stack(
children: [
if (themeType == AppThemeType.doodle)
Positioned.fill(child: CustomPaint(painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)))),
SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// --- SEZIONE 1: IDENTITÀ ---
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: themeType == AppThemeType.doodle ? inkColor : theme.playerBlue.withOpacity(0.3), width: 2),
boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: inkColor.withOpacity(0.8), offset: const Offset(4, 4))] : [],
),
child: Column(
children: [
CircleAvatar(radius: 40, backgroundColor: theme.playerBlue.withOpacity(0.2), child: Icon(Icons.person, size: 45, color: theme.playerBlue)),
const SizedBox(height: 15),
Text(playerName, style: getSharedTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? inkColor : theme.text))),
const SizedBox(height: 5),
Text(title, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: theme.playerRed))),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isAnon ? Icons.warning_amber_rounded : Icons.verified_user, color: isAnon ? Colors.orange : Colors.green, size: 18),
const SizedBox(width: 5),
Text(isAnon ? "Account non protetto" : "Account protetto sul Cloud", style: getSharedTextStyle(themeType, TextStyle(color: isAnon ? Colors.orange : Colors.green, fontWeight: FontWeight.bold, fontSize: 12))),
],
)
],
),
),
const SizedBox(height: 20),
// --- SEZIONE 2: STATISTICHE AVANZATE ---
Row(
children: [
Expanded(child: _buildStatCard("Vittorie", "$wins", Icons.emoji_events, Colors.amber, theme, themeType)),
const SizedBox(width: 15),
Expanded(child: _buildStatCard("Sconfitte", "$losses", Icons.sentiment_very_dissatisfied, theme.playerRed, theme, themeType)),
],
),
const SizedBox(height: 15),
_buildStatCard("Win Rate Globale", "${winRate.toStringAsFixed(1)}%", Icons.pie_chart, theme.playerBlue, theme, themeType, isWide: true),
const SizedBox(height: 25),
// --- SEZIONE 3: REGISTRAZIONE (Solo se anonimo) ---
if (isAnon) ...[
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.orange, width: 2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text("Metti al sicuro i tuoi progressi!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : Colors.white, fontWeight: FontWeight.bold, fontSize: 16))),
const SizedBox(height: 15),
TextField(
controller: _nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8,
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
decoration: InputDecoration(hintText: "Scegli un Nome", hintStyle: TextStyle(color: Colors.grey.withOpacity(0.6)), filled: true, fillColor: Colors.black12, counterText: "", border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none)),
),
const SizedBox(height: 10),
TextField(
controller: _passController, obscureText: _obscurePassword, textAlign: TextAlign.center, maxLength: 20,
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
decoration: InputDecoration(
hintText: "Scegli Password", hintStyle: TextStyle(color: Colors.grey.withOpacity(0.6)), filled: true, fillColor: Colors.black12, counterText: "", border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none),
suffixIcon: IconButton(icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off, color: Colors.grey), onPressed: () => setState(() => _obscurePassword = !_obscurePassword)),
),
),
if (_errorMessage.isNotEmpty) ...[
const SizedBox(height: 10),
Text(_errorMessage, textAlign: TextAlign.center, style: const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold)),
],
if (_nameSuggestions.isNotEmpty) ...[
const SizedBox(height: 10),
Wrap(
spacing: 8, alignment: WrapAlignment.center,
children: _nameSuggestions.map((s) => ActionChip(
label: Text(s, style: const TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: theme.playerBlue.withOpacity(0.2),
side: BorderSide(color: theme.playerBlue),
onPressed: () { _nameController.text = s; _handleRegistration(); },
)).toList(),
)
],
const SizedBox(height: 15),
_isLoading
? const Center(child: CircularProgressIndicator(color: Colors.orange))
: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: _handleRegistration,
child: Text("SALVA PROFILO", style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.5))),
)
],
),
),
const SizedBox(height: 25),
],
// --- SEZIONE 4: IMPOSTAZIONI PRIVACY ---
Text("PRIVACY", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.6) : theme.text.withOpacity(0.5), letterSpacing: 1.5))),
const SizedBox(height: 10),
SwitchListTile(
contentPadding: EdgeInsets.zero,
activeColor: theme.playerBlue,
title: Text("Modalità Fantasma", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontWeight: FontWeight.bold))),
subtitle: Text("Nessuno ti vedrà online o potrà invitarti.", style: TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.6) : theme.text.withOpacity(0.5), fontSize: 12)),
value: _isGhostMode,
onChanged: _toggleGhostMode,
),
const Divider(),
// --- SEZIONE 5: GESTIONE PREFERITI ---
const SizedBox(height: 15),
Text("AMICI PREFERITI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.6) : theme.text.withOpacity(0.5), letterSpacing: 1.5))),
const SizedBox(height: 10),
_buildFavoritesList(theme, themeType, inkColor),
const SizedBox(height: 40),
// --- SEZIONE 6: DANGER ZONE ---
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(color: Colors.redAccent, width: 2),
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
),
icon: const Icon(Icons.delete_forever),
label: Text("ELIMINA PROFILO", style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.5))),
onPressed: _deleteAccount,
),
const SizedBox(height: 30),
],
),
),
if (_isLoading && !isAnon)
Positioned.fill(child: Container(color: Colors.black54, child: const Center(child: CircularProgressIndicator(color: Colors.redAccent)))),
],
),
);
}
Widget _buildStatCard(String title, String value, IconData icon, Color color, ThemeColors theme, AppThemeType themeType, {bool isWide = false}) {
Color inkColor = const Color(0xFF111122);
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: themeType == AppThemeType.doodle ? inkColor : color.withOpacity(0.3), width: 1.5),
boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: inkColor.withOpacity(0.8), offset: const Offset(3, 3))] : [],
),
child: Column(
crossAxisAlignment: isWide ? CrossAxisAlignment.center : CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 8),
Text(title, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.6) : theme.text.withOpacity(0.6), fontSize: 11, fontWeight: FontWeight.bold))),
],
),
const SizedBox(height: 10),
Center(
child: Text(value, style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? inkColor : theme.text))),
),
],
),
);
}
Widget _buildFavoritesList(ThemeColors theme, AppThemeType themeType, Color inkColor) {
final favs = StorageService.instance.favorites;
if (favs.isEmpty) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(15), border: Border.all(color: Colors.grey.withOpacity(0.3), style: BorderStyle.solid)),
child: Center(child: Text("Nessun amico salvato.", style: TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.5) : theme.text.withOpacity(0.5)))),
);
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: favs.length,
itemBuilder: (ctx, i) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.02),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.2) : theme.text.withOpacity(0.1)),
),
child: ListTile(
leading: Icon(Icons.star, color: Colors.amber.shade600),
title: Text(favs[i]['name']!, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontWeight: FontWeight.bold))),
trailing: IconButton(
icon: const Icon(Icons.close, color: Colors.redAccent),
onPressed: () async {
await StorageService.instance.toggleFavorite(favs[i]['uid']!, favs[i]['name']!);
setState(() {});
},
),
),
);
},
);
}
}
class FullScreenGridPainter extends CustomPainter {
final Color gridColor;
FullScreenGridPainter(this.gridColor);
@override
void paint(Canvas canvas, Size size) {
final Paint paperGridPaint = Paint()..color = gridColor..strokeWidth = 1.0..style = PaintingStyle.stroke;
double paperStep = 20.0;
for (double i = 0; i <= size.width; i += paperStep) canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
for (double i = 0; i <= size.height; i += paperStep) canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
}
@override bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View file

@ -2,13 +2,11 @@
// FILE: lib/ui/settings/settings_screen.dart // FILE: lib/ui/settings/settings_screen.dart
// =========================================================================== // ===========================================================================
import 'dart:ui'; // <--- IMPORTANTE: Aggiunto per ImageFilter.blur
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; 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});
@ -22,126 +20,72 @@ 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(
toolbarHeight: 80 * vScale, title: Text("SELEZIONA TEMA", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text)),
title: Container(
padding: EdgeInsets.symmetric(horizontal: 24 * vScale, vertical: 10 * vScale),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(0.3),
Colors.white.withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.white.withOpacity(0.5), width: 1.5),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: 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: Colors.white, size: 28 * vScale), iconTheme: IconThemeData(color: theme.text),
), ),
body: Stack( body: ListView(
padding: const EdgeInsets.all(20),
children: [ children: [
Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background), _ThemeCard(
title: "Minimal",
Positioned.fill( subtitle: "Linee pulite, sfondo chiaro",
child: Container( type: AppThemeType.minimal,
decoration: BoxDecoration( previewColors: AppColors.minimal,
image: DecorationImage( requiredLevel: 1,
image: const AssetImage('assets/images/sfondo_temi.jpg'), currentLevel: playerLevel,
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.6), BlendMode.darken),
),
),
),
), ),
const SizedBox(height: 15),
ListView( _ThemeCard(
padding: EdgeInsets.only(top: 120 * vScale, left: 20 * vScale, right: 20 * vScale, bottom: 40 * vScale), title: "Legno & Fiammiferi",
physics: const BouncingScrollPhysics(), subtitle: "Tavolo di legno, linee come fiammiferi",
children: [ type: AppThemeType.wood,
_ThemeCard( previewColors: AppColors.wood,
title: "Quaderno", requiredLevel: 3,
subtitle: "Sfondo a quadretti, tratto a penna", currentLevel: playerLevel,
type: AppThemeType.doodle, ),
previewColors: AppColors.doodle, const SizedBox(height: 15),
requiredLevel: 1, _ThemeCard(
currentLevel: playerLevel, title: "Quaderno (Doodle)",
vScale: vScale, subtitle: "Sfondo a quadretti, tratto a penna",
), type: AppThemeType.doodle,
SizedBox(height: 25 * vScale), previewColors: AppColors.doodle,
_ThemeCard( requiredLevel: 5,
title: "Cyberpunk", currentLevel: playerLevel,
subtitle: "Nero profondo, luci al neon", ),
type: AppThemeType.cyberpunk, const SizedBox(height: 15),
previewColors: AppColors.cyberpunk, _ThemeCard(
requiredLevel: 3, title: "Cyberpunk",
currentLevel: playerLevel, subtitle: "Nero profondo, luci al neon",
vScale: vScale, type: AppThemeType.cyberpunk,
), previewColors: AppColors.cyberpunk,
SizedBox(height: 25 * vScale), requiredLevel: 7,
_ThemeCard( currentLevel: playerLevel,
title: "8-Bit Arcade", ),
subtitle: "Sale giochi, fosfori verdi e pixel", const SizedBox(height: 15),
type: AppThemeType.arcade, _ThemeCard(
previewColors: AppColors.arcade, title: "8-Bit Arcade",
requiredLevel: 7, subtitle: "Sale giochi, fosfori verdi e pixel",
currentLevel: playerLevel, type: AppThemeType.arcade,
vScale: vScale, previewColors: AppColors.arcade,
), requiredLevel: 10,
SizedBox(height: 25 * vScale), currentLevel: playerLevel,
_ThemeCard( ),
title: "Grimorio", const SizedBox(height: 15),
subtitle: "Incantesimi antichi, rune magiche", _ThemeCard(
type: AppThemeType.grimorio, title: "Grimorio",
previewColors: AppColors.grimorio, subtitle: "Incantesimi antichi, rune magiche",
requiredLevel: 10, type: AppThemeType.grimorio,
currentLevel: playerLevel, previewColors: AppColors.grimorio,
vScale: vScale, requiredLevel: 15,
), currentLevel: playerLevel,
SizedBox(height: 25 * vScale),
_ThemeCard(
title: "Musica",
subtitle: "Vinili, cassette e vibrazioni sonore",
type: AppThemeType.music,
previewColors: AppColors.music,
requiredLevel: 15,
currentLevel: playerLevel,
vScale: vScale,
),
SizedBox(height: 40 * vScale),
],
), ),
], ],
), ),
@ -156,7 +100,6 @@ 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,
@ -165,7 +108,6 @@ 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
@ -174,32 +116,6 @@ 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;
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) {
@ -214,137 +130,63 @@ 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),
height: 140 * vScale, padding: const EdgeInsets.all(20),
padding: EdgeInsets.symmetric(horizontal: 20 * vScale, vertical: 15 * vScale),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isLocked ? Colors.black87 : previewColors.background, color: isLocked ? previewColors.background.withOpacity(0.4) : previewColors.background,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: border, border: Border.all(
boxShadow: shadows, color: isSelected
image: bgImage != null ? DecorationImage( ? previewColors.playerBlue
image: AssetImage(bgImage!), : (isLocked ? Colors.grey.withOpacity(0.3) : previewColors.gridLine.withOpacity(0.5)),
fit: BoxFit.cover, width: isSelected ? 4 : 2,
colorFilter: type == AppThemeType.doodle ),
? ColorFilter.mode(Colors.white.withOpacity(isLocked ? 0.9 : 0.4), BlendMode.lighten) boxShadow: isSelected ? [BoxShadow(color: previewColors.playerBlue.withOpacity(0.4), blurRadius: 10, spreadRadius: 2)] : [],
: 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.3 : 1.0, opacity: isLocked ? 0.25 : 1.0,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
// --- CORNICE EFFETTO VETRO (GLASSMORPHISM) --- child: Column(
child: ClipRRect( crossAxisAlignment: CrossAxisAlignment.start,
borderRadius: BorderRadius.circular(12), children: [
child: BackdropFilter( Text(title, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: previewColors.text)),
filter: ImageFilter.blur(sigmaX: 6.0, sigmaY: 6.0), Text(subtitle, style: TextStyle(fontSize: 14, color: previewColors.text.withOpacity(0.7))),
child: Container( ],
padding: EdgeInsets.symmetric(horizontal: 15 * vScale, vertical: 10 * vScale),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(0.5), // Più visibile in alto a sx
Colors.white.withOpacity(0.1), // Quasi trasparente in basso a dx
],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.3), width: 1.5),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
title,
style: getSharedTextStyle(
type,
TextStyle(
fontSize: (type == AppThemeType.arcade ? 15 : 22) * vScale,
fontWeight: FontWeight.w900,
color: const Color(0xFF111122),
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),
fontWeight: FontWeight.bold,
)
)
),
),
],
),
),
),
), ),
), ),
SizedBox(width: 15 * vScale), Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerRed, shape: BoxShape.circle)),
Container( const SizedBox(width: 10),
width: 28 * vScale, height: 28 * vScale, Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerBlue, shape: BoxShape.circle)),
decoration: BoxDecoration(
color: previewColors.playerRed,
shape: type == AppThemeType.arcade ? BoxShape.rectangle : BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
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),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 5, offset: const Offset(1, 2))]
)
),
], ],
), ),
), ),
if (isLocked) if (isLocked)
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 16 * vScale, vertical: 10 * vScale), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withOpacity(0.95), color: Colors.black.withOpacity(0.85),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: previewColors.playerRed.withOpacity(0.8), width: 2), border: Border.all(color: Colors.white.withOpacity(0.2), width: 1.5),
boxShadow: [BoxShadow(color: previewColors.playerRed.withOpacity(0.5), blurRadius: 20)], boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 10, offset: const Offset(0, 4))],
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.lock_rounded, color: Colors.white, size: 20 * vScale), const Icon(Icons.lock_rounded, color: Colors.white, size: 20),
SizedBox(width: 8 * vScale), const SizedBox(width: 8),
Text( Text(
"LIV. $requiredLevel", "LIV. $requiredLevel",
style: getSharedTextStyle(type, TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 16 * vScale, letterSpacing: 2)) style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 2)
), ),
], ],
), ),

View file

@ -1,244 +1,63 @@
// ===========================================================================
// FILE: lib/widgets/custom_settings_button.dart
// ===========================================================================
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../core/app_colors.dart'; import '../theme/app_colors.dart';
import 'painters.dart'; // Importiamo i painter per i doodle e il font
// Widget per i pulsanti di selezione della forma dell'arena
class NeonShapeButton extends StatelessWidget { class NeonShapeButton extends StatelessWidget {
final IconData icon; final String label; final bool isSelected; final IconData icon;
final ThemeColors theme; final AppThemeType themeType; final VoidCallback onTap; final String label;
final bool isLocked; final bool isSpecial; final bool isSelected;
const NeonShapeButton({super.key, required this.icon, required this.label, required this.isSelected, required this.theme, required this.themeType, required this.onTap, this.isLocked = false, this.isSpecial = false});
Color _getDoodleColor() {
switch (label) {
case 'Rombo': return Colors.lightBlue.shade200;
case 'Croce': return Colors.green.shade200;
case 'Buco': return Colors.pink.shade200;
case 'Clessidra': return Colors.purple.shade200;
case 'Caos': return Colors.grey.shade300;
default: return Colors.lightBlue.shade200;
}
}
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
Color doodleColor = isLocked ? Colors.grey : _getDoodleColor();
Color inkColor = const Color(0xFF111122);
double tilt = (label.length % 2 == 0) ? -0.05 : 0.04;
return Transform.rotate(
angle: tilt,
child: GestureDetector(
onTap: isLocked ? null : onTap,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: isSelected ? doodleColor : Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: label.length * 3),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(isLocked ? Icons.lock : icon, color: inkColor, size: 24),
const SizedBox(height: 2),
Text(isLocked ? "Liv. 10" : label, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.5))),
],
),
),
),
),
);
}
Color mainColor = isSpecial && !isLocked ? Colors.purpleAccent : theme.playerBlue;
return GestureDetector(
onTap: isLocked ? null : onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), transform: Matrix4.translationValues(0, isSelected ? 2 : 0, 0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15), border: Border.all(color: isLocked ? Colors.transparent : (isSelected ? mainColor : Colors.white.withOpacity(0.1)), width: isSelected ? 2 : 1),
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isLocked ? [Colors.grey.withOpacity(0.1), Colors.black.withOpacity(0.2)] : isSelected ? [mainColor.withOpacity(0.3), mainColor.withOpacity(0.1)] : [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)]),
boxShadow: isLocked ? [] : isSelected ? [BoxShadow(color: mainColor.withOpacity(0.5), blurRadius: 15, spreadRadius: 1, offset: const Offset(0, 0))] : [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)), BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1))],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(isLocked ? Icons.lock : icon, color: isLocked ? Colors.grey.withOpacity(0.5) : (isSelected ? Colors.white : theme.text.withOpacity(0.6)), size: 24),
const SizedBox(height: 6),
Text(isLocked ? "Liv. 10" : label, style: getSharedTextStyle(themeType, TextStyle(color: isLocked ? Colors.grey.withOpacity(0.5) : (isSelected ? Colors.white : theme.text.withOpacity(0.6)), fontSize: 11, fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold))),
],
),
),
);
}
}
class NeonSizeButton extends StatelessWidget {
final String label; final bool isSelected; final ThemeColors theme; final AppThemeType themeType; final VoidCallback onTap;
const NeonSizeButton({super.key, required this.label, required this.isSelected, required this.theme, required this.themeType, required this.onTap});
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
Color doodleColor = label == 'MAX' ? Colors.red.shade200 : Colors.cyan.shade100; Color inkColor = const Color(0xFF111122); double tilt = (label == 'M' || label == 'MAX') ? 0.05 : -0.04;
return Transform.rotate(
angle: tilt,
child: GestureDetector(
onTap: onTap,
child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: isSelected ? doodleColor : Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: label.codeUnitAt(0), isCircle: true), child: SizedBox(width: 50, height: 50, child: Center(child: Text(label, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 18, fontWeight: FontWeight.w900)))))),
),
);
}
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, width: 50, height: 50, transform: Matrix4.translationValues(0, isSelected ? 2 : 0, 0),
decoration: BoxDecoration(
shape: BoxShape.circle, border: Border.all(color: isSelected ? theme.playerRed : Colors.white.withOpacity(0.1), width: isSelected ? 2 : 1),
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isSelected ? [theme.playerRed.withOpacity(0.3), theme.playerRed.withOpacity(0.1)] : [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)]),
boxShadow: isSelected ? [BoxShadow(color: theme.playerRed.withOpacity(0.5), blurRadius: 15, spreadRadius: 1)] : [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)), BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1))],
),
child: Center(child: Text(label, style: getSharedTextStyle(themeType, TextStyle(color: isSelected ? Colors.white : theme.text.withOpacity(0.6), fontSize: 14, fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold)))),
),
);
}
}
class NeonTimeSwitch extends StatelessWidget {
final bool isTimeMode; final ThemeColors theme; final AppThemeType themeType; final VoidCallback onTap;
const NeonTimeSwitch({super.key, required this.isTimeMode, required this.theme, required this.themeType, required this.onTap});
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
Color doodleColor = Colors.orange.shade200; Color inkColor = const Color(0xFF111122);
return Transform.rotate(
angle: -0.015,
child: GestureDetector(
onTap: onTap,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: isTimeMode ? doodleColor : Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: 42),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Row(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isTimeMode ? Icons.timer : Icons.timer_off, color: inkColor, size: 28), const SizedBox(width: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [Text(isTimeMode ? 'A TEMPO' : 'RELAX', style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 2.0))), Text(isTimeMode ? '15 sec a mossa' : 'Nessun limite', style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 13, fontWeight: FontWeight.bold)))]),
],
),
),
),
),
);
}
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20), border: Border.all(color: isTimeMode ? Colors.amber : Colors.white.withOpacity(0.1), width: isTimeMode ? 2 : 1),
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isTimeMode ? [Colors.amber.withOpacity(0.25), Colors.amber.withOpacity(0.05)] : [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)]),
boxShadow: isTimeMode ? [BoxShadow(color: Colors.amber.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)] : [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)), BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1))],
),
child: Row(
mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isTimeMode ? Icons.timer : Icons.timer_off, color: isTimeMode ? Colors.amber : theme.text.withOpacity(0.5), size: 28), const SizedBox(width: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [Text(isTimeMode ? 'A TEMPO' : 'RELAX', style: getSharedTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : theme.text.withOpacity(0.5), fontWeight: FontWeight.w900, fontSize: 14, letterSpacing: 1.5))), Text(isTimeMode ? '15 sec a mossa' : 'Nessun limite', style: getSharedTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.amber.shade200 : theme.text.withOpacity(0.4), fontSize: 11, fontWeight: FontWeight.bold)))]),
],
),
),
);
}
}
class NeonPrivacySwitch extends StatelessWidget {
final bool isPublic;
final ThemeColors theme;
final AppThemeType themeType;
final VoidCallback onTap; final VoidCallback onTap;
final ShapeBorder shape; // La forma geometrica del pulsante
const NeonPrivacySwitch({super.key, required this.isPublic, required this.theme, required this.themeType, required this.onTap}); const NeonShapeButton({
super.key,
required this.icon,
required this.label,
required this.isSelected,
required this.onTap,
this.shape = const RoundedRectangleBorder( // Forma di default
borderRadius: BorderRadius.all(Radius.circular(12.0))),
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
Color doodleColor = isPublic ? Colors.green.shade600 : Colors.red.shade600;
return Transform.rotate(
angle: 0.015,
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
transform: Matrix4.translationValues(0, isPublic ? 3 : 0, 0),
decoration: BoxDecoration(
color: isPublic ? doodleColor : Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15), topRight: Radius.circular(8),
bottomLeft: Radius.circular(6), bottomRight: Radius.circular(15),
),
border: Border.all(color: isPublic ? theme.text : doodleColor.withOpacity(0.5), width: 2.5),
boxShadow: [BoxShadow(color: isPublic ? theme.text.withOpacity(0.8) : doodleColor.withOpacity(0.2), offset: const Offset(4, 5), blurRadius: 0)],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.white : doodleColor, size: 20),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(isPublic ? 'PUBBLICA' : 'PRIVATA', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : doodleColor, fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 1.0))),
Text(isPublic ? 'In Bacheca' : 'Solo Codice', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : doodleColor.withOpacity(0.8), fontSize: 9, fontWeight: FontWeight.bold))),
],
),
],
),
),
),
);
}
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: ShapeDecoration(
borderRadius: BorderRadius.circular(15), shape: shape,
gradient: LinearGradient( color: isSelected
begin: Alignment.topLeft, ? AppColors.neonGreen.withOpacity(0.2) // Sfondo luminoso se selezionato
end: Alignment.bottomRight, : AppColors.surface.withOpacity(0.5), // Sfondo più scuro se non selezionato
colors: isPublic shadows: isSelected
? [Colors.greenAccent.withOpacity(0.25), Colors.greenAccent.withOpacity(0.05)] ? [ // Bagliore intenso se selezionato
: [theme.playerRed.withOpacity(0.25), theme.playerRed.withOpacity(0.05)], BoxShadow(
), color: AppColors.neonGreen.withOpacity(0.6),
border: Border.all(color: isPublic ? Colors.greenAccent : theme.playerRed, width: isPublic ? 2 : 1), blurRadius: 12.0,
boxShadow: isPublic spreadRadius: 2.0,
? [BoxShadow(color: Colors.greenAccent.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)] ),
: [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4))], ]
: [],
), ),
child: Row( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.greenAccent : theme.playerRed, size: 20), Icon(
const SizedBox(width: 8), icon,
Column( color: isSelected ? AppColors.neonGreen : AppColors.textSecondary,
crossAxisAlignment: CrossAxisAlignment.start, size: 28,
mainAxisSize: MainAxisSize.min, ),
children: [ const SizedBox(height: 4),
Text(isPublic ? 'PUBBLICA' : 'PRIVATA', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : theme.text.withOpacity(0.8), fontWeight: FontWeight.w900, fontSize: 11, letterSpacing: 1.5))), Text(
Text(isPublic ? 'Tutti ti vedono' : 'Solo con Codice', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.greenAccent.shade200 : theme.playerRed.withOpacity(0.7), fontSize: 9, fontWeight: FontWeight.bold))), label,
], style: TextStyle(
color: isSelected ? AppColors.textPrimary : AppColors.textSecondary,
fontSize: 12,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
), ),
], ],
), ),
@ -247,71 +66,118 @@ class NeonPrivacySwitch extends StatelessWidget {
} }
} }
class NeonActionButton extends StatelessWidget { // Widget per i pulsanti di selezione della taglia dell'arena
class NeonSizeButton extends StatelessWidget {
final String label; final String label;
final Color color; final bool isSelected;
final VoidCallback onTap; final VoidCallback onTap;
final ThemeColors theme;
final AppThemeType themeType;
const NeonActionButton({super.key, required this.label, required this.color, required this.onTap, required this.theme, required this.themeType}); const NeonSizeButton({
super.key,
required this.label,
required this.isSelected,
required this.onTap,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
double tilt = (label == "UNISCITI" || label == "ANNULLA") ? -0.015 : 0.02;
return Transform.rotate(
angle: tilt,
child: GestureDetector(
onTap: onTap,
child: Container(
height: 50,
decoration: BoxDecoration(
color: color,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10), topRight: Radius.circular(20),
bottomLeft: Radius.circular(25), bottomRight: Radius.circular(10),
),
border: Border.all(color: theme.text, width: 3.0),
boxShadow: [BoxShadow(color: theme.text.withOpacity(0.9), offset: const Offset(4, 4), blurRadius: 0)],
),
child: Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: Colors.white))),
),
),
),
),
),
);
}
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: 50,
height: 50, height: 50,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [color.withOpacity(0.9), color.withOpacity(0.6)]), shape: BoxShape.circle, // Forma circolare
borderRadius: BorderRadius.circular(15), color: isSelected
border: Border.all(color: Colors.white.withOpacity(0.3), width: 1.5), ? AppColors.neonBlue.withOpacity(0.2)
boxShadow: [ : AppColors.surface.withOpacity(0.5),
BoxShadow(color: Colors.black.withOpacity(0.5), offset: const Offset(4, 8), blurRadius: 12), border: Border.all(
BoxShadow(color: color.withOpacity(0.3), offset: const Offset(0, 0), blurRadius: 15, spreadRadius: 1), color: isSelected ? AppColors.neonBlue : AppColors.surfaceLight,
], width: 2.0,
),
shadows: isSelected
? [
BoxShadow(
color: AppColors.neonBlue.withOpacity(0.6),
blurRadius: 10.0,
spreadRadius: 1.5,
),
]
: [],
), ),
child: Center( child: Center(
child: FittedBox( child: Text(
fit: BoxFit.scaleDown, label,
child: Padding( style: TextStyle(
padding: const EdgeInsets.symmetric(horizontal: 10.0), color: isSelected ? AppColors.textPrimary : AppColors.textSecondary,
child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white, shadows: [Shadow(color: Colors.black, blurRadius: 2, offset: Offset(1, 1))]))), fontSize: 16,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
), ),
), ),
), ),
), ),
); );
} }
}
// Widget per l'interruttore della modalità tempo (Clessidra)
class NeonTimeSwitch extends StatelessWidget {
final bool isTimeMode;
final VoidCallback onTap;
const NeonTimeSwitch({
super.key,
required this.isTimeMode,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.0), // Forma arrotondata per lo switch
color: isTimeMode
? AppColors.neonGreen.withOpacity(0.2)
: AppColors.surface.withOpacity(0.5),
border: Border.all(
color: isTimeMode ? AppColors.neonGreen : AppColors.surfaceLight,
width: 2.0,
),
shadows: isTimeMode
? [
BoxShadow(
color: AppColors.neonGreen.withOpacity(0.6),
blurRadius: 12.0,
spreadRadius: 2.0,
),
]
: [],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.hourglass_empty, // Icona clessidra
color: isTimeMode ? AppColors.neonGreen : AppColors.textSecondary,
),
const SizedBox(width: 8),
Text(
isTimeMode ? 'A TEMPO' : 'SENZA TEMPO',
style: TextStyle(
color: isTimeMode ? AppColors.textPrimary : AppColors.textSecondary,
fontWeight: isTimeMode ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
);
}
} }

View file

@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../core/theme_manager.dart'; // Import aggiornato
import 'dart:math' as math;
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: theme.background.withOpacity(0.9), borderRadius: BorderRadius.circular(15), boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 25, 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(15));
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 = 4.0
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 4);
canvas.drawRRect(rrect, paint);
}
@override bool shouldRepaint(covariant CyberBorderPainter oldDelegate) => oldDelegate.animationValue != animationValue;
}

View file

@ -1,14 +1,8 @@
// ===========================================================================
// FILE: lib/widgets/game_over_dialog.dart
// ===========================================================================
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../logic/game_controller.dart'; import '../logic/game_controller.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 'painters.dart';
class GameOverDialog extends StatelessWidget { class GameOverDialog extends StatelessWidget {
const GameOverDialog({super.key}); const GameOverDialog({super.key});
@ -19,25 +13,21 @@ class GameOverDialog extends StatelessWidget {
final themeManager = context.read<ThemeManager>(); final themeManager = context.read<ThemeManager>();
final theme = themeManager.currentColors; final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType; final themeType = themeManager.currentThemeType;
Color inkColor = const Color(0xFF111122);
int red = game.board.scoreRed; int red = game.board.scoreRed;
int blue = game.board.scoreBlue; int blue = game.board.scoreBlue;
bool playerBeatCPU = game.isVsCPU && red > blue; bool playerBeatCPU = game.isVsCPU && red > blue;
String myName = StorageService.instance.playerName.toUpperCase();
if (myName.isEmpty) myName = "TU";
// --- LOGICA NOMI --- // --- LOGICA NOMI ---
String nameRed = myName; String nameRed = "ROSSO";
String nameBlue = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU"; String nameBlue = themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU";
if (game.isOnline) { if (game.isOnline) {
nameRed = game.onlineHostName.toUpperCase(); nameRed = game.onlineHostName.toUpperCase();
nameBlue = game.onlineGuestName.toUpperCase(); nameBlue = game.onlineGuestName.toUpperCase();
} else if (game.isVsCPU) { } else if (game.isVsCPU) {
nameRed = myName; nameRed = "TU";
nameBlue = "CPU"; nameBlue = "CPU";
} }
@ -53,269 +43,118 @@ class GameOverDialog extends StatelessWidget {
winnerColor = theme.playerBlue; winnerColor = theme.playerBlue;
} else { } else {
winnerText = "PAREGGIO!"; winnerText = "PAREGGIO!";
winnerColor = themeType == AppThemeType.doodle ? inkColor : theme.text; winnerColor = theme.text;
} }
Widget dialogContent = Column( return AlertDialog(
mainAxisSize: MainAxisSize.min, backgroundColor: theme.background,
children: [ shape: RoundedRectangleBorder(
Text(winnerText, textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))), borderRadius: BorderRadius.circular(20),
const SizedBox(height: 20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2),
Container( ),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), title: Text("FINE PARTITA", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22)),
decoration: BoxDecoration( content: Column(
color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05), mainAxisSize: MainAxisSize.min,
borderRadius: BorderRadius.circular(15), children: [
border: themeType == AppThemeType.doodle ? Border.all(color: inkColor.withOpacity(0.3), width: 1.5) : null, Text(winnerText, textAlign: TextAlign.center, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor)),
), const SizedBox(height: 20),
child: FittedBox( Container(
fit: BoxFit.scaleDown, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(15),
),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text("$nameRed: $red", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))), Text("$nameRed: $red", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed)),
Text(" - ", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, color: themeType == AppThemeType.doodle ? inkColor : theme.text))), Text(" - ", style: TextStyle(fontSize: 18, color: theme.text)),
Text("$nameBlue: $blue", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))), Text("$nameBlue: $blue", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue)),
], ],
), ),
), ),
),
if (game.lastMatchXP > 0) ...[ if (game.isVsCPU) ...[
const SizedBox(height: 15), const SizedBox(height: 15),
Container( Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7))),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), ]
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, width: 1.5),
boxShadow: (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : [],
),
child: Text("+ ${game.lastMatchXP} XP", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))),
),
], ],
),
if (game.isVsCPU) ...[ actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10),
const SizedBox(height: 15), actionsAlignment: MainAxisAlignment.center,
Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.7) : theme.text.withOpacity(0.7)))), actions: [
],
if (game.isOnline) ...[
const SizedBox(height: 20),
if (game.rematchRequested && !game.opponentWantsRematch)
Text("In attesa di $nameBlue...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.orange.shade700 : Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))),
if (game.opponentWantsRematch && !game.rematchRequested)
Text("$nameBlue vuole la rivincita!", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.bold))),
if (game.rematchRequested && game.opponentWantsRematch)
Text("Avvio nuova partita...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade800 : Colors.green, fontWeight: FontWeight.bold))),
],
// --- SEZIONE LEVEL UP E ROADMAP DINAMICA ---
if (game.hasLeveledUp && game.unlockedRewards.isNotEmpty) ...[
const SizedBox(height: 30),
Divider(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.3) : theme.text.withOpacity(0.2)),
const SizedBox(height: 15),
Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.amber.withOpacity(0.1) : Colors.amber.withOpacity(0.2),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, width: 2)
),
child: Text("🎉 LIVELLO ${game.newlyReachedLevel}! 🎉", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, fontWeight: FontWeight.w900, fontSize: 18))),
),
const SizedBox(height: 15),
...game.unlockedRewards.map((reward) {
Color rewardColor = themeType == AppThemeType.doodle ? (reward['color'] as Color).withOpacity(0.8) : reward['color'];
return Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: rewardColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: rewardColor.withOpacity(0.5), width: 1.5),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: rewardColor.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(reward['icon'], color: rewardColor, size: 28),
),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(reward['title'], style: getSharedTextStyle(themeType, TextStyle(color: rewardColor, fontWeight: FontWeight.w900, fontSize: 16))),
const SizedBox(height: 4),
Text(reward['desc'], style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.9) : theme.text.withOpacity(0.9), fontSize: 12, height: 1.3))),
],
)
)
]
),
);
}),
],
const SizedBox(height: 30),
// --- BOTTONI AZIONE ---
Column( Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (playerBeatCPU) if (playerBeatCPU)
_buildPrimaryButton( ElevatedButton(
"PROSSIMO LIVELLO ➔", style: ElevatedButton.styleFrom(
winnerColor, backgroundColor: winnerColor,
themeType, foregroundColor: Colors.white,
inkColor, padding: const EdgeInsets.symmetric(vertical: 15),
() { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 5,
),
onPressed: () {
Navigator.pop(context); Navigator.pop(context);
game.increaseLevelAndRestart(); game.increaseLevelAndRestart();
}, },
child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
) )
else if (game.isOnline) else if (game.isOnline)
_buildPrimaryButton( ElevatedButton(
game.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: ElevatedButton.styleFrom(
game.rematchRequested ? Colors.grey : (winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor), backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor,
themeType, foregroundColor: Colors.white,
inkColor, padding: const EdgeInsets.symmetric(vertical: 15),
game.rematchRequested ? () {} : () => game.requestRematch(), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 5,
),
onPressed: () {
Navigator.pop(context);
if (game.board.isGameOver) {
game.requestRematch();
}
},
child: const Text("RIGIOCA ONLINE", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5)),
) )
else else
_buildPrimaryButton( ElevatedButton(
"RIGIOCA", style: ElevatedButton.styleFrom(
winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor, backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor,
themeType, foregroundColor: Colors.white,
inkColor, padding: const EdgeInsets.symmetric(vertical: 15),
() { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 5,
),
onPressed: () {
Navigator.pop(context); Navigator.pop(context);
game.startNewGame(game.board.radius, vsCPU: game.isVsCPU); game.startNewGame(game.board.radius, vsCPU: game.isVsCPU);
}, },
child: const Text("RIGIOCA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildSecondaryButton( OutlinedButton(
"TORNA AL MENU", style: OutlinedButton.styleFrom(
themeType, foregroundColor: theme.text,
inkColor, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2),
theme, padding: const EdgeInsets.symmetric(vertical: 15),
() { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
),
onPressed: () {
if (game.isOnline) { if (game.isOnline) {
game.disconnectOnlineGame(); game.disconnectOnlineGame();
} }
Navigator.pop(context); Navigator.pop(context);
Navigator.pop(context); Navigator.pop(context);
}, },
child: Text("TORNA AL MENU", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)),
), ),
], ],
) )
], ],
); );
if (themeType == AppThemeType.doodle) {
dialogContent = Transform.rotate(
angle: 0.015,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 500),
child: Padding(
padding: const EdgeInsets.all(25.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2))),
const SizedBox(height: 20),
dialogContent,
],
),
),
),
);
} else {
dialogContent = Container(
padding: const EdgeInsets.all(25.0),
decoration: BoxDecoration(
color: theme.background,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: winnerColor.withOpacity(0.5), width: 2),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: theme.text))),
const SizedBox(height: 20),
dialogContent,
],
),
);
}
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: dialogContent,
);
}
Widget _buildPrimaryButton(String label, Color color, AppThemeType themeType, Color inkColor, VoidCallback onTap) {
if (themeType == AppThemeType.doodle) {
return GestureDetector(
onTap: onTap,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: color, strokeColor: inkColor, seed: label.length * 7),
child: Container(
height: 55,
alignment: Alignment.center,
child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 1.5))),
),
),
);
}
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 5,
),
onPressed: onTap,
child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5))),
);
}
Widget _buildSecondaryButton(String label, AppThemeType themeType, Color inkColor, ThemeColors theme, VoidCallback onTap) {
if (themeType == AppThemeType.doodle) {
return GestureDetector(
onTap: onTap,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: Colors.transparent, strokeColor: inkColor.withOpacity(0.5), seed: label.length * 3),
child: Container(
height: 55,
alignment: Alignment.center,
child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 1.5))),
),
),
);
}
return OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: theme.text,
side: BorderSide(color: theme.text.withOpacity(0.3), width: 2),
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
),
onPressed: onTap,
child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))),
);
} }
} }

View file

@ -1,90 +0,0 @@
import 'package:flutter/material.dart';
import '../core/app_colors.dart'; // Import aggiornato
import 'painters.dart';
class FeatureCard extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Color color;
final ThemeColors theme;
final AppThemeType themeType;
final VoidCallback onTap;
final bool isFeatured;
final bool compact;
const FeatureCard({super.key, required this.title, required this.subtitle, required this.icon, required this.color, required this.theme, required this.themeType, required this.onTap, this.isFeatured = false, this.compact = false});
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
double tilt = (title.length % 2 == 0) ? -0.015 : 0.02;
Color inkColor = const Color(0xFF111122);
return Transform.rotate(
angle: tilt,
child: GestureDetector(
onTap: onTap,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: color, strokeColor: inkColor, seed: title.length * 5),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: compact ? 12.0 : 22.0, vertical: compact ? 12.0 : 16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(icon, color: inkColor, size: compact ? 24 : 32),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(title, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: compact ? 16 : 24, fontWeight: FontWeight.w900)))),
if (!compact) ...[ const SizedBox(height: 2), Text(subtitle, style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.bold))) ]
],
),
),
if (!compact) Icon(Icons.chevron_right_rounded, color: inkColor.withOpacity(0.6), size: 32),
],
),
),
),
),
);
}
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: compact ? 12.0 : 20.0, vertical: compact ? 10.0 : 14.0),
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isFeatured ? [color.withOpacity(0.9), color.withOpacity(0.6)] : [color.withOpacity(0.25), color.withOpacity(0.05)]),
borderRadius: BorderRadius.circular(15), border: Border.all(color: color.withOpacity(isFeatured ? 0.5 : 0.2), width: 1.5),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.6), offset: const Offset(0, 8), blurRadius: 15), BoxShadow(color: color.withOpacity(isFeatured ? 0.3 : 0.05), offset: const Offset(-1, -1), blurRadius: 5)]
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: EdgeInsets.all(compact ? 6 : 10),
decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.white.withOpacity(0.3), Colors.white.withOpacity(0.05)]), shape: BoxShape.circle, border: Border.all(color: Colors.white.withOpacity(0.2)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 5, offset: const Offset(2, 4))]),
child: Icon(icon, color: isFeatured ? Colors.white : color, size: compact ? 20 : 26),
),
SizedBox(width: compact ? 10 : 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(title, style: getSharedTextStyle(themeType, TextStyle(color: isFeatured ? Colors.white : theme.text, fontSize: compact ? 14 : 18, fontWeight: FontWeight.w900, shadows: [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 2), blurRadius: 2)])))),
if (!compact) ...[ const SizedBox(height: 2), Text(subtitle, style: getSharedTextStyle(themeType, TextStyle(color: isFeatured ? Colors.white.withOpacity(0.8) : theme.text.withOpacity(0.6), fontSize: 12, fontWeight: FontWeight.bold))) ]
],
),
),
if (!compact) Icon(Icons.chevron_right_rounded, color: isFeatured ? Colors.white.withOpacity(0.7) : color.withOpacity(0.5), size: 30),
],
),
),
);
}
}

View file

@ -1,210 +0,0 @@
// ===========================================================================
// FILE: lib/widgets/music_theme_widgets.dart
// ===========================================================================
import 'package:flutter/material.dart';
import '../core/app_colors.dart';
import 'painters.dart';
class MusicCassetteCard extends StatelessWidget {
final String title;
final String subtitle;
final Color neonColor;
final double angle;
final IconData leftIcon;
final IconData rightIcon;
final VoidCallback onTap;
final AppThemeType themeType;
const MusicCassetteCard({
super.key,
required this.title,
required this.subtitle,
required this.neonColor,
required this.angle,
required this.leftIcon,
required this.rightIcon,
required this.onTap,
required this.themeType
});
@override
Widget build(BuildContext context) {
// Calcoliamo la scala in base all'altezza dello schermo per strizzare la cassetta
final double screenHeight = MediaQuery.of(context).size.height;
final double vScale = (screenHeight / 850.0).clamp(0.65, 1.0);
return Transform.rotate(
angle: angle,
child: GestureDetector(
onTap: onTap,
child: Container(
height: 125 * vScale, // Altezza dinamica!
margin: EdgeInsets.symmetric(vertical: 8 * vScale, horizontal: 10),
padding: EdgeInsets.all(12 * vScale),
decoration: BoxDecoration(
color: const Color(0xFF22222A),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.black87, width: 2),
boxShadow: [
BoxShadow(color: neonColor.withOpacity(0.5), blurRadius: 25, spreadRadius: 2),
const BoxShadow(color: Colors.black54, offset: Offset(5, 10), blurRadius: 15)
]
),
child: Column(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: neonColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: neonColor.withOpacity(0.5), width: 1.5)
),
child: Row(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 12 * vScale),
child: Icon(leftIcon, color: neonColor, size: 28 * vScale)
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(title, style: getSharedTextStyle(themeType, TextStyle(color: Colors.white, fontSize: 20 * vScale, fontWeight: FontWeight.w900, shadows: [Shadow(color: neonColor, blurRadius: 10)])))
)
),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(subtitle, style: getSharedTextStyle(themeType, TextStyle(color: Colors.white70, fontSize: 11 * vScale, fontWeight: FontWeight.bold)))
)
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12 * vScale),
child: Icon(rightIcon, color: neonColor, size: 28 * vScale)
),
],
),
),
),
SizedBox(height: 10 * vScale),
Container(
height: 35 * vScale,
width: 180 * vScale,
decoration: BoxDecoration(
color: const Color(0xFF0D0D12),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white24, width: 1)
),
child: Stack(
alignment: Alignment.center,
children: [
Container(height: 2, width: 120 * vScale, color: const Color(0xFF333333)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ _buildSpool(vScale), _buildSpool(vScale) ]
),
],
),
),
],
),
),
),
);
}
Widget _buildSpool(double vScale) {
return Container(
width: 26 * vScale,
height: 26 * vScale,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white70,
border: Border.all(color: Colors.black87, width: 5 * vScale)
),
child: Center(
child: Container(
width: 6 * vScale,
height: 6 * vScale,
decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.black)
)
),
);
}
}
class MusicKnobCard extends StatelessWidget {
final String title;
final IconData icon;
final VoidCallback onTap;
final AppThemeType themeType;
final Color? iconColor;
const MusicKnobCard({
super.key,
required this.title,
required this.icon,
required this.onTap,
required this.themeType,
this.iconColor
});
@override
Widget build(BuildContext context) {
// Adattiamo anche le manopole in base all'altezza dello schermo
final double screenHeight = MediaQuery.of(context).size.height;
final double vScale = (screenHeight / 850.0).clamp(0.65, 1.0);
return GestureDetector(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 65 * vScale,
height: 65 * vScale,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFF222222),
border: Border.all(color: const Color(0xFF111111), width: 2),
boxShadow: const [
BoxShadow(color: Colors.black87, blurRadius: 10, offset: Offset(2, 6)),
BoxShadow(color: Colors.white12, blurRadius: 2, offset: Offset(-1, -1))
],
),
child: Padding(
padding: EdgeInsets.all(6.0 * vScale),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.black54, width: 1),
gradient: const SweepGradient(colors: [Color(0xFF555555), Color(0xFFAAAAAA), Color(0xFF555555), Color(0xFF222222), Color(0xFF555555)]),
),
child: Padding(
padding: EdgeInsets.all(4.0 * vScale),
child: Container(
decoration: const BoxDecoration(shape: BoxShape.circle, color: Color(0xFF1A1A1A)),
child: Center(child: Icon(icon, color: iconColor ?? Colors.white70, size: 20 * vScale)),
),
),
),
),
),
SizedBox(height: 10 * vScale),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(title, style: getSharedTextStyle(themeType, TextStyle(color: Colors.white70, fontSize: 11 * vScale, fontWeight: FontWeight.bold, letterSpacing: 1.0)))
),
],
),
);
}
}

View file

@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math;
import '../core/app_colors.dart'; // Import aggiornato
TextStyle getSharedTextStyle(AppThemeType themeType, TextStyle baseStyle) {
if (themeType == AppThemeType.doodle) {
return GoogleFonts.permanentMarker(textStyle: baseStyle);
} else if (themeType == AppThemeType.arcade) {
return GoogleFonts.pressStart2p(textStyle: baseStyle.copyWith(fontSize: baseStyle.fontSize != null ? baseStyle.fontSize! * 0.75 : null, letterSpacing: 0.5));
} else if (themeType == AppThemeType.grimorio) {
return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold));
} else if (themeType == AppThemeType.music) {
return GoogleFonts.audiowide(textStyle: baseStyle.copyWith(letterSpacing: 1.5));
}
return baseStyle;
}
class DoodleBackgroundPainter extends CustomPainter {
final Color fillColor; final Color strokeColor; final int seed; final bool isCircle;
DoodleBackgroundPainter({required this.fillColor, required this.strokeColor, required this.seed, this.isCircle = false});
@override
void paint(Canvas canvas, Size size) {
final math.Random random = math.Random(seed);
double wobble() => random.nextDouble() * 6 - 3;
final Paint fillPaint = Paint()..color = fillColor..style = PaintingStyle.fill;
final Paint strokePaint = Paint()..color = strokeColor..strokeWidth = 2.5..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round;
if (isCircle) {
final Rect rect = Rect.fromLTWH(wobble(), wobble(), size.width + wobble(), size.height + wobble());
canvas.save(); canvas.translate(wobble(), wobble()); canvas.drawOval(rect, fillPaint); canvas.restore();
canvas.drawOval(rect, strokePaint);
canvas.save(); canvas.translate(random.nextDouble() * 4 - 2, random.nextDouble() * 4 - 2); canvas.drawOval(rect, strokePaint..strokeWidth = 1.0..color = strokeColor.withOpacity(0.6)); canvas.restore();
} else {
final Path path = Path()..moveTo(wobble(), wobble())..lineTo(size.width + wobble(), wobble())..lineTo(size.width + wobble(), size.height + wobble())..lineTo(wobble(), size.height + wobble())..close();
final Path fillPath = Path()..moveTo(wobble() * 1.5, wobble() * 1.5)..lineTo(size.width + wobble() * 1.5, wobble() * 1.5)..lineTo(size.width + wobble() * 1.5, size.height + wobble() * 1.5)..lineTo(wobble() * 1.5, size.height + wobble() * 1.5)..close();
canvas.drawPath(fillPath, fillPaint);
canvas.drawPath(path, strokePaint);
canvas.save(); canvas.translate(random.nextDouble() * 3 - 1.5, random.nextDouble() * 3 - 1.5); canvas.drawPath(path, strokePaint..strokeWidth = 1.0..color = strokeColor.withOpacity(0.6)); canvas.restore();
}
}
@override bool shouldRepaint(covariant DoodleBackgroundPainter oldDelegate) => oldDelegate.fillColor != fillColor || oldDelegate.strokeColor != strokeColor;
}
class AudioCablesPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = const Color(0xFF151515)..style = PaintingStyle.stroke..strokeWidth = 8.0..strokeCap = StrokeCap.round;
final highlight = Paint()..color = const Color(0xFF3A3A3A)..style = PaintingStyle.stroke..strokeWidth = 2.0..strokeCap = StrokeCap.round;
void drawCable(Path path) { canvas.drawPath(path, paint); canvas.drawPath(path, highlight); }
Path c1 = Path()..moveTo(-20, size.height * 0.2)..quadraticBezierTo(100, size.height * 0.25, 50, size.height * 0.4)..quadraticBezierTo(0, size.height * 0.5, -20, size.height * 0.55); drawCable(c1);
Path c2 = Path()..moveTo(size.width + 20, size.height * 0.4)..quadraticBezierTo(size.width - 100, size.height * 0.5, size.width - 50, size.height * 0.7)..quadraticBezierTo(size.width, size.height * 0.8, size.width + 20, size.height * 0.85); drawCable(c2);
Path c3 = Path()..moveTo(size.width * 0.2, size.height + 20)..quadraticBezierTo(size.width * 0.3, size.height - 80, size.width * 0.5, size.height - 60)..quadraticBezierTo(size.width * 0.7, size.height - 40, size.width * 0.8, size.height + 20); drawCable(c3);
_drawJack(canvas, Offset(80, size.height * 0.38), -0.5);
_drawJack(canvas, Offset(size.width - 60, size.height * 0.68), 0.8);
}
void _drawJack(Canvas canvas, Offset pos, double angle) {
canvas.save(); canvas.translate(pos.dx, pos.dy); canvas.rotate(angle);
canvas.drawRect(const Rect.fromLTWH(-15, -4, 15, 8), Paint()..color = const Color(0xFF151515));
canvas.drawRRect(RRect.fromRectAndRadius(const Rect.fromLTWH(0, -6, 25, 12), const Radius.circular(2)), Paint()..color = const Color(0xFF222222));
canvas.drawRRect(RRect.fromRectAndRadius(const Rect.fromLTWH(2, -4, 21, 8), const Radius.circular(2)), Paint()..color = const Color(0xFF444444));
canvas.drawRect(const Rect.fromLTWH(25, -2, 15, 4), Paint()..color = const Color(0xFFCCCCCC));
canvas.drawRect(const Rect.fromLTWH(40, -1.5, 5, 3), Paint()..color = const Color(0xFFAAAAAA));
canvas.drawLine(const Offset(30, -2), const Offset(30, 2), Paint()..color = Colors.black..strokeWidth = 1.5);
canvas.drawLine(const Offset(35, -2), const Offset(35, 2), Paint()..color = Colors.black..strokeWidth = 1.5);
canvas.restore();
}
@override bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

BIN
macos/.DS_Store vendored

Binary file not shown.

View file

@ -8,25 +8,15 @@ import Foundation
import app_links import app_links
import audioplayers_darwin import audioplayers_darwin
import cloud_firestore import cloud_firestore
import device_info_plus
import firebase_app_check
import firebase_auth
import firebase_core import firebase_core
import package_info_plus
import share_plus import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View file

@ -1190,10 +1190,6 @@ PODS:
- abseil/xcprivacy (1.20240722.0) - abseil/xcprivacy (1.20240722.0)
- app_links (6.4.1): - app_links (6.4.1):
- FlutterMacOS - FlutterMacOS
- AppCheckCore (11.2.0):
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- audioplayers_darwin (0.0.1): - audioplayers_darwin (0.0.1):
- FlutterMacOS - FlutterMacOS
- BoringSSL-GRPC (0.0.37): - BoringSSL-GRPC (0.0.37):
@ -1203,67 +1199,33 @@ PODS:
- BoringSSL-GRPC/Interface (= 0.0.37) - BoringSSL-GRPC/Interface (= 0.0.37)
- BoringSSL-GRPC/Interface (0.0.37) - BoringSSL-GRPC/Interface (0.0.37)
- cloud_firestore (6.1.2): - cloud_firestore (6.1.2):
- Firebase/CoreOnly (~> 12.9.0) - Firebase/CoreOnly (~> 12.8.0)
- Firebase/Firestore (~> 12.9.0) - Firebase/Firestore (~> 12.8.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- device_info_plus (0.0.1): - Firebase/CoreOnly (12.8.0):
- FlutterMacOS - FirebaseCore (~> 12.8.0)
- Firebase/AppCheck (12.9.0): - Firebase/Firestore (12.8.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAppCheck (~> 12.9.0) - FirebaseFirestore (~> 12.8.0)
- Firebase/Auth (12.9.0): - firebase_core (4.4.0):
- Firebase/CoreOnly - Firebase/CoreOnly (~> 12.8.0)
- FirebaseAuth (~> 12.9.0)
- Firebase/CoreOnly (12.9.0):
- FirebaseCore (~> 12.9.0)
- Firebase/Firestore (12.9.0):
- Firebase/CoreOnly
- FirebaseFirestore (~> 12.9.0)
- firebase_app_check (0.4.1-5):
- Firebase/AppCheck (~> 12.9.0)
- Firebase/CoreOnly (~> 12.9.0)
- firebase_core
- FlutterMacOS - FlutterMacOS
- firebase_auth (6.1.4): - FirebaseAppCheckInterop (12.8.0)
- Firebase/Auth (~> 12.9.0) - FirebaseCore (12.8.0):
- Firebase/CoreOnly (~> 12.9.0) - FirebaseCoreInternal (~> 12.8.0)
- firebase_core
- FlutterMacOS
- firebase_core (4.5.0):
- Firebase/CoreOnly (~> 12.9.0)
- FlutterMacOS
- FirebaseAppCheck (12.9.0):
- AppCheckCore (~> 11.0)
- FirebaseAppCheckInterop (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- FirebaseAppCheckInterop (12.9.0)
- FirebaseAuth (12.9.0):
- FirebaseAppCheckInterop (~> 12.9.0)
- FirebaseAuthInterop (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- FirebaseCoreExtension (~> 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GTMSessionFetcher/Core (< 6.0, >= 3.4)
- RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (12.9.0)
- FirebaseCore (12.9.0):
- FirebaseCoreInternal (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1) - GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.9.0): - FirebaseCoreExtension (12.8.0):
- FirebaseCore (~> 12.9.0) - FirebaseCore (~> 12.8.0)
- FirebaseCoreInternal (12.9.0): - FirebaseCoreInternal (12.8.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseFirestore (12.9.0): - FirebaseFirestore (12.8.0):
- FirebaseCore (~> 12.9.0) - FirebaseCore (~> 12.8.0)
- FirebaseCoreExtension (~> 12.9.0) - FirebaseCoreExtension (~> 12.8.0)
- FirebaseFirestoreInternal (~> 12.9.0) - FirebaseFirestoreInternal (~> 12.8.0)
- FirebaseSharedSwift (~> 12.9.0) - FirebaseSharedSwift (~> 12.8.0)
- FirebaseFirestoreInternal (12.9.0): - FirebaseFirestoreInternal (12.8.0):
- abseil/algorithm (~> 1.20240722.0) - abseil/algorithm (~> 1.20240722.0)
- abseil/base (~> 1.20240722.0) - abseil/base (~> 1.20240722.0)
- abseil/container/flat_hash_map (~> 1.20240722.0) - abseil/container/flat_hash_map (~> 1.20240722.0)
@ -1272,38 +1234,22 @@ PODS:
- abseil/strings/strings (~> 1.20240722.0) - abseil/strings/strings (~> 1.20240722.0)
- abseil/time (~> 1.20240722.0) - abseil/time (~> 1.20240722.0)
- abseil/types (~> 1.20240722.0) - abseil/types (~> 1.20240722.0)
- FirebaseAppCheckInterop (~> 12.9.0) - FirebaseAppCheckInterop (~> 12.8.0)
- FirebaseCore (~> 12.9.0) - FirebaseCore (~> 12.8.0)
- "gRPC-C++ (~> 1.69.0)" - "gRPC-C++ (~> 1.69.0)"
- gRPC-Core (~> 1.69.0) - gRPC-Core (~> 1.69.0)
- leveldb-library (~> 1.22) - leveldb-library (~> 1.22)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseSharedSwift (12.9.0) - FirebaseSharedSwift (12.8.0)
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0): - GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0): - GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment - GoogleUtilities/Environment
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)": - "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0) - GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- "gRPC-C++ (1.69.0)": - "gRPC-C++ (1.69.0)":
- "gRPC-C++/Implementation (= 1.69.0)" - "gRPC-C++/Implementation (= 1.69.0)"
- "gRPC-C++/Interface (= 1.69.0)" - "gRPC-C++/Interface (= 1.69.0)"
@ -1396,48 +1342,33 @@ PODS:
- gRPC-Core/Privacy (= 1.69.0) - gRPC-Core/Privacy (= 1.69.0)
- gRPC-Core/Interface (1.69.0) - gRPC-Core/Interface (1.69.0)
- gRPC-Core/Privacy (1.69.0) - gRPC-Core/Privacy (1.69.0)
- GTMSessionFetcher/Core (5.1.0)
- leveldb-library (1.22.6) - leveldb-library (1.22.6)
- nanopb (3.30910.0): - nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0) - nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0) - nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0) - nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0) - nanopb/encode (3.30910.0)
- package_info_plus (0.0.1):
- FlutterMacOS
- PromisesObjC (2.4.0)
- share_plus (0.0.1): - share_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
- audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`) - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`)
- cloud_firestore (from `Flutter/ephemeral/.symlinks/plugins/cloud_firestore/macos`) - cloud_firestore (from `Flutter/ephemeral/.symlinks/plugins/cloud_firestore/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- firebase_app_check (from `Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos`)
- firebase_auth (from `Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos`)
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- abseil - abseil
- AppCheckCore
- BoringSSL-GRPC - BoringSSL-GRPC
- Firebase - Firebase
- FirebaseAppCheck
- FirebaseAppCheckInterop - FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore - FirebaseCore
- FirebaseCoreExtension - FirebaseCoreExtension
- FirebaseCoreInternal - FirebaseCoreInternal
@ -1447,10 +1378,8 @@ SPEC REPOS:
- GoogleUtilities - GoogleUtilities
- "gRPC-C++" - "gRPC-C++"
- gRPC-Core - gRPC-Core
- GTMSessionFetcher
- leveldb-library - leveldb-library
- nanopb - nanopb
- PromisesObjC
EXTERNAL SOURCES: EXTERNAL SOURCES:
app_links: app_links:
@ -1459,59 +1388,38 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos
cloud_firestore: cloud_firestore:
:path: Flutter/ephemeral/.symlinks/plugins/cloud_firestore/macos :path: Flutter/ephemeral/.symlinks/plugins/cloud_firestore/macos
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
firebase_app_check:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos
firebase_auth:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos
firebase_core: firebase_core:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
share_plus: share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation: shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
abseil: a05cc83bf02079535e17169a73c5be5ba47f714b abseil: a05cc83bf02079535e17169a73c5be5ba47f714b
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
audioplayers_darwin: 761f2948df701d05b5db603220c384fb55720012 audioplayers_darwin: 761f2948df701d05b5db603220c384fb55720012
BoringSSL-GRPC: dded2a44897e45f28f08ae87a55ee4bcd19bc508 BoringSSL-GRPC: dded2a44897e45f28f08ae87a55ee4bcd19bc508
cloud_firestore: a2a9382e6cc4dd07345748b904b3b194ea46be44 cloud_firestore: 71947b640bd24f6f849d9d185e5d0a619fa6b93b
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56 firebase_core: b1697fb64ff2b9ca16baaa821205f8b0c058e5d2
firebase_app_check: 1ea404b52b0910bf632b1ea2e00ceb8d1730cb44 FirebaseAppCheckInterop: ba3dc604a89815379e61ec2365101608d365cf7d
firebase_auth: 8db6796451d9aa44d4cc49b3e757865c65ce170f FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c
firebase_core: c74b220e9288decea6bed17399c249734a7e76d2 FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2
FirebaseAppCheck: 94dae4d9bb682bdef85a778b0c1024a4613f1e89 FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21
FirebaseAppCheckInterop: 4bade10286cc977e516f75d2d8312cbdfa534789 FirebaseFirestore: 67f23000ca238ccbab79127ed59636a9a2689e74
FirebaseAuth: 3a39f6436c21ebfd7919b698228b4f89ff94c23b FirebaseFirestoreInternal: a0e7382af3d208898dcd1d4d52d8a7870632e881
FirebaseAuthInterop: f8f6ff72dc24621906497fbe5cf3c42ee815e59c FirebaseSharedSwift: f57ed48f4542b2d7eb4738f4f23ba443f78b3780
FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
FirebaseCoreExtension: e911052d59cd0da237a45d706fc0f81654f035c1
FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
FirebaseFirestore: d8b76ca1feb4ca0b0f078c45f7d1bd8014a49ef1
FirebaseFirestoreInternal: 02341a9ba87f6309227b04685022a5e16307bbf7
FirebaseSharedSwift: 9d2fa84a46676302b89dbd5e6e62bce2fe376909
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
"gRPC-C++": cc207623316fb041a7a3e774c252cf68a058b9e8 "gRPC-C++": cc207623316fb041a7a3e774c252cf68a058b9e8
gRPC-Core: 860978b7db482de8b4f5e10677216309b5ff6330 gRPC-Core: 860978b7db482de8b4f5e10677216309b5ff6330
GTMSessionFetcher: b8ab00db932816e14b0a0664a08cb73dda6d164b
leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009

View file

@ -2,15 +2,13 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.cs.allow-jit</key> <key>com.apple.security.cs.allow-jit</key>
<true/> <true/>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>
<true/> <true/>
<key>com.apple.security.network.server</key> <key>com.apple.security.network.server</key>
<true/> <true/>
<key>keychain-access-groups</key>
<array/>
</dict> </dict>
</plist> </plist>

View file

@ -6,22 +6,10 @@ class MainFlutterWindow: NSWindow {
let flutterViewController = FlutterViewController() let flutterViewController = FlutterViewController()
let windowFrame = self.frame let windowFrame = self.frame
self.contentViewController = flutterViewController self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
// 1. Definiamo le proporzioni esatte da smartphone
let phoneSize = NSSize(width: 400, height: 800)
let newRect = NSRect(origin: windowFrame.origin, size: phoneSize)
self.setFrame(newRect, display: true)
// 2. Blocchiamo il ridimensionamento! Il Mac non potrà più allargarla a dismisura
self.minSize = phoneSize
self.maxSize = phoneSize
// 3. IL TRUCCO MAGICO: Cambiamo il nome del salvataggio automatico.
// Questo costringe macOS a dimenticare la vecchia finestra larga e usare questa nuova.
self.setFrameAutosaveName("TetraQMobileSimulatorWindow")
RegisterGeneratedPlugins(registry: flutterViewController) RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib() super.awakeFromNib()
} }
} }

View file

@ -8,7 +8,5 @@
<true/> <true/>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>
<true/> <true/>
<key>keychain-access-groups</key>
<array/>
</dict> </dict>
</plist> </plist>

View file

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page Not Found</title>
<style media="screen">
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
#message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
@media (max-width: 600px) {
body, #message { margin-top: 0; background: white; box-shadow: none; }
body { border-top: 16px solid #ffa100; }
}
</style>
</head>
<body>
<div id="message">
<h2>404</h2>
<h1>Page Not Found</h1>
<p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
<h3>Why am I seeing this?</h3>
<p>This page was generated by the Firebase Command-Line Interface. To modify it, edit the <code>404.html</code> file in your project's configured <code>public</code> directory.</p>
</div>
</body>
</html>

View file

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gioca a TetraQ!</title>
<meta property="og:title" content="Gioca a TetraQ!">
<meta property="og:description" content="Sfida i tuoi amici nell'arena al neon. Unisciti alla partita!">
<meta property="og:image" content="https://upload.wikimedia.org/wikipedia/commons/c/ca/1x1.png">
<script>
function redirect() {
var userAgent = navigator.userAgent || navigator.vendor || window.opera;
// Se è iOS
if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
window.location.href = "https://apps.apple.com/it/app/tetraq/id6759522394";
return;
}
// Se è Android
if (/android/i.test(userAgent)) {
window.location.href = "https://play.google.com/store/apps/details?id=com.amastra.tetraq";
return;
}
// Se è da PC (o non riconosciuto), lo mandiamo alla tua pagina Google Sites
window.location.href = "https://sites.google.com/view/tetraq/home-page";
}
</script>
</head>
<body onload="redirect()" style="background-color: #0A001A; color: white;">
<h3 style="text-align: center; font-family: sans-serif; margin-top: 50px;">
Apertura in corso... 🚀
</h3>
</body>
</html>

View file

@ -1,448 +0,0 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Report Giocatori - TetraQ</title>
<script src="/__/firebase/10.8.0/firebase-app-compat.js"></script>
<script src="/__/firebase/10.8.0/firebase-auth-compat.js"></script>
<script src="/__/firebase/10.8.0/firebase-firestore-compat.js"></script>
<script src="/__/firebase/init.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f8f9fa;
color: #2c3e50;
padding: 20px;
margin: 0;
box-sizing: border-box;
}
/* --- STILI LOGIN --- */
#login-view {
background-color: #34495e;
position: fixed;
top: 0; left: 0; width: 100%; height: 100vh;
display: flex; justify-content: center; align-items: center;
z-index: 1000; padding: 20px; box-sizing: border-box;
}
.login-box {
background: white; padding: 40px; border-radius: 12px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1); text-align: center;
width: 100%; max-width: 350px; box-sizing: border-box;
}
.login-box h2 { color: #2c3e50; margin-top: 0; font-weight: 800; letter-spacing: 1px;}
.login-box input {
width: 100%; padding: 12px; margin: 10px 0 20px 0;
border: 1px solid #e1e5eb; border-radius: 8px; box-sizing: border-box;
font-size: 16px; outline: none; transition: border-color 0.2s;
}
.login-box input:focus { border-color: #3498db; }
.login-box button {
width: 100%; background-color: #e74c3c; color: white;
border: none; padding: 12px; font-size: 16px; border-radius: 8px;
cursor: pointer; font-weight: bold; transition: background-color 0.2s;
}
.login-box button:hover { background-color: #c0392b; }
.login-box button:disabled { background-color: #95a5a6; cursor: not-allowed; }
.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 15px rgba(0,0,0,0.05);
box-sizing: border-box;
}
.header-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
h1 { color: #2c3e50; margin: 0; font-size: 26px; font-weight: 800;}
.btn-logout {
background-color: transparent; color: #95a5a6; padding: 8px 15px;
border-radius: 5px; font-weight: bold; font-size: 14px;
border: 1px solid #ecf0f1; cursor: pointer; transition: 0.2s;
}
.btn-logout:hover { background-color: #f9f9f9; color: #e74c3c; border-color: #e74c3c; }
/* --- FILTRI --- */
.filter-section {
background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px;
display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;
border: 1px solid #e1e5eb;
}
.form-group { display: flex; flex-direction: column; flex: 1; min-width: 150px; }
.form-group label { font-size: 12px; font-weight: bold; color: #7f8c8d; margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.5px;}
.form-group input, .form-group select {
padding: 10px; border: 1px solid #bdc3c7; border-radius: 6px;
font-size: 14px; outline: none; box-sizing: border-box; width: 100%;
}
.form-group input:focus, .form-group select:focus { border-color: #3498db; }
.btn-filtra {
background-color: #3498db; color: white; padding: 10px 20px; border: none;
border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 14px;
transition: 0.2s; height: 40px;
}
.btn-filtra:hover { background-color: #2980b9; }
.btn-reset {
background-color: #95a5a6; color: white; padding: 0 15px; border: none;
border-radius: 6px; cursor: pointer; font-size: 14px; height: 40px;
line-height: 40px; text-align: center;
}
.btn-reset:hover { background-color: #7f8c8d; }
/* --- CARDS RIASSUNTIVE --- */
.dashboard { display: flex; justify-content: space-between; margin-bottom: 30px; gap: 20px; text-align: center; flex-wrap: wrap; }
.card {
flex: 1; min-width: 150px; background: white; padding: 20px;
border-radius: 8px; border-left: 5px solid #3498db; box-sizing: border-box;
box-shadow: 0 2px 8px rgba(0,0,0,0.05); border: 1px solid #e1e5eb; border-left-width: 5px;
}
.card.ios { border-left-color: #e74c3c; }
.card.android { border-left-color: #2ecc71; }
.card h3 { margin: 0 0 10px 0; font-size: 12px; color: #7f8c8d; text-transform: uppercase; letter-spacing: 0.5px;}
.card p { margin: 0; font-size: 28px; font-weight: 800; color: #2c3e50; }
/* --- TABELLA --- */
table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 14px; table-layout: fixed; }
th, td { padding: 16px 20px; text-align: left; vertical-align: top;}
th {
background-color: #34495e; color: white; font-weight: 700;
font-size: 13px; letter-spacing: 0.5px;
}
th:first-child { border-top-left-radius: 6px; border-bottom-left-radius: 6px; }
th:last-child { border-top-right-radius: 6px; border-bottom-right-radius: 6px; }
td { border-bottom: 1px solid #ecf0f1; color: #2c3e50; }
tr:hover td { background-color: #fbfcfc; }
th:nth-child(1) { width: 15%; }
th:nth-child(2) { width: 25%; }
th:nth-child(3) { width: 15%; }
th:nth-child(4) { width: 25%; }
th:nth-child(5) { width: 20%; }
.player-name { font-weight: 800; color: #2c3e50; font-size: 15px; }
.player-level { font-weight: normal; color: #e74c3c; }
.sub-text { display: block; font-size: 12px; color: #95a5a6; margin-top: 4px; font-weight: 500; }
.data-text { font-weight: 500; color: #2c3e50; font-size: 14px; }
.stat-value { font-weight: bold; color: #3498db; }
.badge {
padding: 4px 8px; border-radius: 4px; color: white;
font-weight: 800; font-size: 11px; text-transform: uppercase;
letter-spacing: 0.5px; display: inline-block; margin-bottom: 5px;
}
.badge-ios { background-color: #e74c3c; }
.badge-android { background-color: #2ecc71; }
.badge-desktop { background-color: #95a5a6; }
.empty { text-align: center; padding: 40px; color: #95a5a6; font-weight: 500; }
/* 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="email" id="email" autocomplete="username" required>
<input type="password" id="password" placeholder="Password" autocomplete="current-password" required>
<button type="submit" id="login-btn">Accedi al Database</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()">Disconnetti 🚪</button>
</div>
<form class="filter-section" id="filter-form">
<div class="form-group">
<label for="data_da">Ultimo Accesso Da:</label>
<input type="date" id="data_da">
</div>
<div class="form-group">
<label for="data_a">Ultimo Accesso 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">Cerca Giocatore/Città:</label>
<input type="text" id="loc" placeholder="Digita...">
</div>
<button type="submit" class="btn-filtra">🔍 Applica</button>
<button type="button" class="btn-reset" onclick="resetFilters()">Reset</button>
</form>
<div class="dashboard">
<div class="card">
<h3>Totale Giocatori</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 / Mac</h3>
<p id="desktop-text">0</p>
</div>
</div>
<table>
<thead>
<tr>
<th>Ultimo Accesso</th>
<th>Giocatore</th>
<th>Statistiche</th>
<th>Connessione</th>
<th>Dispositivo</th>
</tr>
</thead>
<tbody id="table-body">
<tr><td colspan="5" class="empty">Caricamento dati dal database in corso...</td></tr>
</tbody>
</table>
</div>
</div>
<script>
let allData = [];
let unsubscribeLeaderboard = null;
// --- FUNZIONI DI UTILITÀ ---
function formatTime(seconds) {
if (!seconds || seconds <= 0) return "00:00";
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
return `${h.toString().padStart(2, '0')}h ${m.toString().padStart(2, '0')}m`;
}
function formatDate(dateObj) {
if (!dateObj) return "N/D";
const options = { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' };
return dateObj.toLocaleDateString('it-IT', options).replace(',', '');
}
function formatDateShort(dateObj) {
if (!dateObj) return "N/D";
return dateObj.toLocaleDateString('it-IT', { year: 'numeric', month: '2-digit', day: '2-digit' });
}
// --- LISTENER AUTENTICAZIONE FIREBASE ---
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// L'utente è loggato, mostra la dashboard
document.getElementById('login-view').style.display = 'none';
document.getElementById('dashboard-view').style.display = 'block';
loadFirebaseData();
} else {
// Nessun utente loggato, mostra il login
document.getElementById('dashboard-view').style.display = 'none';
document.getElementById('login-view').style.display = 'flex';
if(unsubscribeLeaderboard) {
unsubscribeLeaderboard(); // Ferma l'ascolto del database se fai logout
}
}
});
// --- GESTIONE LOGIN ---
document.getElementById('login-form').addEventListener('submit', function(e) {
e.preventDefault();
const email = document.getElementById('email').value.trim();
const pwd = document.getElementById('password').value;
const btn = document.getElementById('login-btn');
btn.disabled = true;
btn.innerText = "Accesso in corso...";
document.getElementById('login-error').innerText = "";
// Usa l'Auth sicuro di Firebase
firebase.auth().signInWithEmailAndPassword(email, pwd)
.then(() => {
// Il successo è gestito da onAuthStateChanged
btn.disabled = false;
btn.innerText = "Accedi al Database";
})
.catch((error) => {
btn.disabled = false;
btn.innerText = "Accedi al Database";
let errorMessage = "Errore durante il login.";
if(error.code === 'auth/user-not-found' || error.code === 'auth/wrong-password' || error.code === 'auth/invalid-credential') {
errorMessage = "Email o password errati.";
}
document.getElementById('login-error').innerText = errorMessage;
});
});
function logout() {
firebase.auth().signOut().then(() => {
document.getElementById('email').value = '';
document.getElementById('password').value = '';
document.getElementById('table-body').innerHTML = '<tr><td colspan="5" class="empty">Caricamento dati dal database...</td></tr>';
allData = [];
});
}
// --- CONNESSIONE A FIREBASE FIRESTORE ---
function loadFirebaseData() {
const db = firebase.firestore();
// Salviamo la funzione di unsubscribe per fermare il listener al logout
unsubscribeLeaderboard = db.collection('leaderboard').orderBy('lastActive', 'desc').onSnapshot((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";
}
if (data.accountCreated) {
data.createdObj = data.accountCreated.toDate();
}
if ((data.name || '').toUpperCase() !== 'PIPPO') {
allData.push(data);
}
});
applyFilters();
}, error => {
console.error("Errore lettura database:", error);
// Se c'è un errore (es. permessi negati), forse non siamo admin
if (error.code === 'permission-denied') {
document.getElementById('table-body').innerHTML = '<tr><td colspan="5" class="empty" style="color:#e74c3c;">Accesso Negato: Non hai i permessi per leggere questi dati.</td></tr>';
} else {
document.getElementById('table-body').innerHTML = '<tr><td colspan="5" class="empty" style="color:#e74c3c;">Errore di connessione a Firebase. Riprova.</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 name = (row.name || 'Sconosciuto').toLowerCase();
if (fDa !== '' && row.dateStr < fDa) mostra = false;
if (fA !== '' && row.dateStr > fA) mostra = false;
if (fOs !== '') {
if (fOs === 'Desktop' && (platform === 'iOS' || platform === 'Android')) mostra = false;
if (fOs !== 'Desktop' && platform !== fOs) mostra = false;
}
if (fLoc !== '' && !city.includes(fLoc) && !name.includes(fLoc)) mostra = false;
if (mostra) {
tot++;
let badgeClass = 'badge-desktop';
let platformDisplay = platform;
if (platform === 'iOS' || platform === 'macOS') { ios++; badgeClass = 'badge-ios'; }
else if (platform === 'Android') { android++; badgeClass = 'badge-android'; }
else { desktop++; platformDisplay = 'Desktop'; }
html += `
<tr>
<td data-label="Ultimo Accesso">
<span class="data-text">${formatDate(row.dateObj)}</span>
</td>
<td data-label="Giocatore">
<span class="player-name">${(row.name || 'GUEST').toUpperCase()}</span>
<span class="player-level">(Liv. ${row.level || 1})</span>
<span class="sub-text">Iscritto il: ${formatDateShort(row.createdObj)}</span>
</td>
<td data-label="Statistiche">
<span class="sub-text">XP: <span class="stat-value">${row.xp || 0}</span></span>
<span class="sub-text">Vittorie: <span class="stat-value" style="color:#2ecc71;">${row.wins || 0}</span></span>
<span class="sub-text">Tempo: <span class="stat-value" style="color:#2c3e50;">${formatTime(row.playtime)}</span></span>
</td>
<td data-label="Connessione">
<span class="badge ${badgeClass}">${platformDisplay}</span>
<span class="loc-text" style="display:block; margin-top:2px;">${row.city || 'N/D'}</span>
<span class="sub-text">IP: ${row.ip || 'N/D'}</span>
</td>
<td data-label="Dispositivo" style="font-size: 13px; color: #7f8c8d;">
<strong>${row.deviceModel || 'N/D'}</strong>
<span class="sub-text">App v. ${row.appVersion || 'N/D'}</span>
</td>
</tr>
`;
}
});
if (tot === 0) {
html = '<tr><td colspan="5" class="empty">Nessun giocatore corrisponde ai filtri selezionati.</td></tr>';
}
document.getElementById('table-body').innerHTML = html;
document.getElementById('totale-text').innerText = tot;
document.getElementById('ios-text').innerText = ios;
document.getElementById('android-text').innerText = android;
document.getElementById('desktop-text').innerText = desktop;
}
</script>
</body>
</html>

View file

@ -5,10 +5,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _flutterfire_internals name: _flutterfire_internals
sha256: afe15ce18a287d2f89da95566e62892df339b1936bbe9b83587df45b944ee72a sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.67" version: "1.3.66"
app_links: app_links:
dependency: "direct main" dependency: "direct main"
description: description:
@ -129,22 +129,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
change_app_package_name:
dependency: "direct dev"
description:
name: change_app_package_name
sha256: "8e43b754fe960426904d77ed4c62fa8c9834deaf6e293ae40963fa447482c4c5"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.1"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@ -225,14 +217,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -241,22 +225,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c"
url: "https://pub.dev"
source: hosted
version: "12.3.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
url: "https://pub.dev"
source: hosted
version: "7.0.3"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -281,62 +249,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
firebase_app_check:
dependency: "direct main"
description:
name: firebase_app_check
sha256: cc0bfeb003c5747e963f4f378e72c8c5c5f91a243f2e85b7da29ff42f4709996
url: "https://pub.dev"
source: hosted
version: "0.4.1+5"
firebase_app_check_platform_interface:
dependency: transitive
description:
name: firebase_app_check_platform_interface
sha256: "54166319f9121bd94b2374864e84dd54507438095fc2d42ca49f23957b16b0d1"
url: "https://pub.dev"
source: hosted
version: "0.2.1+5"
firebase_app_check_web:
dependency: transitive
description:
name: firebase_app_check_web
sha256: f8dddba09bd7786e017b58e74455886106ac7ca9e798769e2cac3e988fc2d146
url: "https://pub.dev"
source: hosted
version: "0.2.2+3"
firebase_auth:
dependency: "direct main"
description:
name: firebase_auth
sha256: b20d1540460814c5984474c1e9dd833bdbcff6ecd8d6ad86cc9da8cfd581c172
url: "https://pub.dev"
source: hosted
version: "6.1.4"
firebase_auth_platform_interface:
dependency: transitive
description:
name: firebase_auth_platform_interface
sha256: fd0225320b6bbc92460c86352d16b60aea15f9ef88292774cca97b0522ea9f72
url: "https://pub.dev"
source: hosted
version: "8.1.6"
firebase_auth_web:
dependency: transitive
description:
name: firebase_auth_web
sha256: be7dccb263b89fbda2a564de9d8193118196e8481ffb937222a025cdfdf82c40
url: "https://pub.dev"
source: hosted
version: "6.1.2"
firebase_core: firebase_core:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
sha256: f0997fee80fbb6d2c658c5b88ae87ba1f9506b5b37126db64fc2e75d8e977fbb sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.0" version: "4.4.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -349,10 +269,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: firebase_core_web name: firebase_core_web
sha256: "856ca92bf2d75a63761286ab8e791bda3a85184c2b641764433b619647acfca6" sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.5.0" version: "3.4.0"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -437,14 +357,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http: http:
dependency: transitive dependency: transitive
description: description:
@ -469,14 +381,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.0" version: "4.8.0"
in_app_update:
dependency: "direct main"
description:
name: in_app_update
sha256: "9924a3efe592e1c0ec89dda3683b3cfec3d4cd02d908e6de00c24b759038ddb1"
url: "https://pub.dev"
source: hosted
version: "4.2.5"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -545,18 +449,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.17" version: "0.12.19"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.13.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -597,30 +501,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.3.0" version: "9.3.0"
os_detect:
dependency: transitive
description:
name: os_detect
sha256: "7d87c0dd98c6faf110d5aa498e9a6df02ffce4bb78cc9cfc8ad02929be9bb71f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
url: "https://pub.dev"
source: hosted
version: "9.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -854,10 +734,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.7" version: "0.7.10"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -866,38 +746,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
upgrader:
dependency: "direct main"
description:
name: upgrader
sha256: "3fae4eb861c7e8567f91412d9ca4a287e024e58d6f79f98da79e3f6d78da74ba"
url: "https://pub.dev"
source: hosted
version: "12.5.0"
url_launcher:
dependency: transitive
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
url: "https://pub.dev"
source: hosted
version: "6.3.28"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@ -906,14 +754,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.2" version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -954,14 +794,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
version:
dependency: transitive
description:
name: version
sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@ -986,14 +818,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.15.0" version: "5.15.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View file

@ -1,7 +1,7 @@
name: tetraq name: tetraq
description: A new Flutter project. description: A new Flutter project.
publish_to: 'none' publish_to: 'none'
version: 1.1.9+4 version: 1.0.2+4
environment: environment:
sdk: ^3.10.7 sdk: ^3.10.7
@ -18,26 +18,17 @@ dependencies:
flutter_localizations: # Il sistema multilingua ufficiale flutter_localizations: # Il sistema multilingua ufficiale
sdk: flutter sdk: flutter
firebase_core: ^4.4.0 firebase_core: ^4.4.0
firebase_auth: ^6.1.4 # <--- NUOVO: LA CORAZZA DI SICUREZZA!
cloud_firestore: ^6.1.2 cloud_firestore: ^6.1.2
share_plus: ^12.0.1 share_plus: ^12.0.1
app_links: ^7.0.0 app_links: ^7.0.0
google_fonts: ^8.0.2 google_fonts: ^8.0.2
font_awesome_flutter: ^10.12.0 font_awesome_flutter: ^10.12.0
firebase_app_check: ^0.4.1+5
package_info_plus: ^9.0.0
device_info_plus: ^12.3.0
# --- NUOVI PACCHETTI PER GLI AGGIORNAMENTI ---
upgrader: ^12.5.0
in_app_update: ^4.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.13.1 flutter_launcher_icons: ^0.13.1
change_app_package_name: ^1.5.0
flutter: flutter:
uses-material-design: true uses-material-design: true
@ -50,7 +41,6 @@ flutter:
- assets/images/ - assets/images/
- assets/audio/bgm/ - assets/audio/bgm/
- assets/audio/sfx/ - assets/audio/sfx/
- assets/audio/
flutter_icons: flutter_icons:

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more