Compare commits
2 commits
main
...
v_20260314
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42b8180f5e | ||
| dfe392ea88 |
BIN
.DS_Store
vendored
|
|
@ -1,3 +0,0 @@
|
||||||
index.html,1773586765860,5737ce966fa8786becaf7f36a32992cf44102fb3a217c226c30576c993b33e63
|
|
||||||
404.html,1773344753356,05cbc6f94d7a69ce2e29646eab13be2c884e61ba93e3094df5028866876d18b3
|
|
||||||
report.html,1774225497103,87e2cc9055f15faf5a6228e0933ea51a1fb147cecdcfd3336df8299474f0126e
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"projects": {
|
|
||||||
"default": "tetraq-32a4a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
16
.metadata
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = "../.."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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
BIN
assets/audio/.DS_Store
vendored
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
BIN
assets/images/wood_bg.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
|
|
@ -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/**"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
155
ios/Podfile.lock
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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',
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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, you’ll need to edit this
|
|
||||||
/// file.
|
|
||||||
///
|
|
||||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
|
||||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
|
||||||
/// project’s 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.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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": "ВЫХОД"
|
|
||||||
}
|
|
||||||
|
|
@ -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": "退出"
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
|
@ -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))),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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); }
|
|
||||||
}
|
}
|
||||||
|
|
@ -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)); }
|
||||||
|
|
|
||||||
|
|
@ -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))),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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))),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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))]))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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))),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)))
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
@ -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"))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
204
pubspec.lock
|
|
@ -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:
|
||||||
|
|
|
||||||
12
pubspec.yaml
|
|
@ -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:
|
||||||
|
|
|
||||||