tetraq/report/TetraQ_20-03-26_21.39.txt
2026-03-24 14:00:01 +01:00

9833 lines
No EOL
436 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

=== FLUTTER PROJECT BACKUP ===
=== PROJECT STRUCTURE (LIB & ASSETS) ===
assets/.DS_Store
assets/audio/.DS_Store
assets/audio/bgm/8-bit_Prowler.mp3
assets/audio/bgm/Cyber_Dystopia.mp3
assets/audio/bgm/Grimorio_Astral.mp3
assets/audio/bgm/Legno_Canopy.mp3
assets/audio/bgm/Music_Loop.mp3
assets/audio/bgm/Quad_Dreams.mp3
assets/audio/sfx/cyber_box.wav
assets/audio/sfx/cyber_line.wav
assets/audio/sfx/doodle_box.wav
assets/audio/sfx/doodle_line.wav
assets/audio/sfx/minimal_box.wav
assets/audio/sfx/minimal_line.wav
assets/icon/icona_master.png
assets/images/.DS_Store
assets/images/arcade.jpg
assets/images/cyber_bg.jpg
assets/images/doodle_bg.jpg
assets/images/doodle_bg_riserva.jpg
assets/images/egizi_bg.jpg
assets/images/grimorio copia.jpg
assets/images/grimorio.jpg
assets/images/icona_big.jpeg
assets/images/music_bg.jpg
assets/images/sfondo_temi.jpg
lib/.DS_Store
lib/core/app_colors.dart
lib/core/constants.dart
lib/core/theme_manager.dart
lib/firebase_options.dart
lib/l10n/app_de.arb
lib/l10n/app_en.arb
lib/l10n/app_es.arb
lib/l10n/app_fr.arb
lib/l10n/app_it.arb
lib/l10n/app_localizations.dart
lib/l10n/app_localizations_de.dart
lib/l10n/app_localizations_en.dart
lib/l10n/app_localizations_es.dart
lib/l10n/app_localizations_fr.dart
lib/l10n/app_localizations_it.dart
lib/l10n/app_localizations_pt.dart
lib/l10n/app_localizations_ru.dart
lib/l10n/app_localizations_zh.dart
lib/l10n/app_pt.arb
lib/l10n/app_ru.arb
lib/l10n/app_zh.arb
lib/logic/ai_engine.dart
lib/logic/game_controller.dart
lib/main.dart
lib/models/game_board.dart
lib/models/player_info.dart
lib/services/audio_service.dart
lib/services/firebase_service.dart
lib/services/multiplayer_service.dart
lib/services/storage_service.dart
lib/ui/.DS_Store
lib/ui/admin/admin_screen.dart
lib/ui/game/board_painter.dart
lib/ui/game/game_screen.dart
lib/ui/game/score_board.dart
lib/ui/home/dialog.dart
lib/ui/home/history_screen.dart
lib/ui/home/home_modals.dart
lib/ui/home/home_screen.dart
lib/ui/multiplayer/lobby_screen.dart
lib/ui/multiplayer/lobby_widgets.dart
lib/ui/settings/settings_screen.dart
lib/widgets/custom_button.dart
lib/widgets/custom_settings_button.dart
lib/widgets/cyber_border.dart
lib/widgets/game_over_dialog.dart
lib/widgets/home_buttons.dart
lib/widgets/music_theme_widgets.dart
lib/widgets/painters.dart
=== pubspec.yaml ===
name: tetraq
description: A new Flutter project.
publish_to: 'none'
version: 1.1.5+7
environment:
sdk: ^3.10.7
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
# I nostri "Superpoteri"
provider: ^6.1.2 # Per gestire lo stato (Temi, Punteggi)
shared_preferences: ^2.5.4 # Per salvare opzioni e record sul telefono
audioplayers: ^5.2.1 # Per la musica e gli effetti sonori
intl: ^0.20.2 # Necessario per le traduzioni
flutter_localizations: # Il sistema multilingua ufficiale
sdk: flutter
firebase_core: ^4.4.0
firebase_auth: ^6.1.4 # <--- NUOVO: LA CORAZZA DI SICUREZZA!
cloud_firestore: ^6.1.2
share_plus: ^12.0.1
app_links: ^7.0.0
google_fonts: ^8.0.2
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:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.13.1
change_app_package_name: ^1.5.0
flutter:
uses-material-design: true
# Abilita la generazione automatica delle traduzioni
generate: true
# Dichiariamo le cartelle dove metteremo immagini e suoni
assets:
- assets/images/
- assets/audio/bgm/
- assets/audio/sfx/
- assets/audio/
flutter_icons:
android: "ic_launcher"
ios: true
macos:
generate: true
image_path: "assets/icon/icona_master.png"
min_sdk_android: 21 # Serve per compatibilità con Android recenti
=== MAC OS CONFIG ===
--- Info.plist ---
<?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>LSApplicationCategoryType</key>
<string>public.app-category.puzzle-games</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>--- Entitlements ---
<?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.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>
<?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.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>
--- Podfile ---
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end
=== IOS CONFIG ===
--- Info.plist ---
<?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>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Tetraq</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>tetraq</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.sanza.tetraq</string>
<key>CFBundleURLSchemes</key>
<array>
<string>tetraq</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<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>
</dict>
</plist>
--- Podfile ---
# Uncomment this line to define a global platform for your project
platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
=== ANDROID CONFIG ===
--- AndroidManifest.xml ---
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="tetraq"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tetraq" android:host="join" />
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>--- build.gradle / build.gradle.kts ---
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
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 {
namespace = "com.amastra.tetraq"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
// Sintassi aggiornata come richiesto dal compilatore Kotlin
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.amastra.tetraq"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
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 {
getByName("release") {
// TODO: Add your own signing config for the release build.
// Ora usiamo la chiave di release appena creata
signingConfig = signingConfigs.getByName("release")
}
}
}
flutter {
source = "../.."
}
=== SOURCE CODE (lib/) ===
// ===========================================================================
// FILE: lib/core/app_colors.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/core/app_colors.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
enum AppThemeType { doodle, cyberpunk, arcade, grimorio, music }
class ThemeColors {
final Color background;
final Color gridLine;
final Color playerRed;
final Color playerBlue;
final Color text;
const ThemeColors({
required this.background,
required this.gridLine,
required this.playerRed,
required this.playerBlue,
required this.text,
});
}
class AppColors {
static const ThemeColors doodle = ThemeColors(
background: Color(0xFFFFF9E6), gridLine: Color(0xFFB0BEC5),
playerRed: Color(0xFFD32F2F), playerBlue: Color(0xFF1565C0), text: Color(0xFF37474F),
);
static const ThemeColors cyberpunk = ThemeColors(
background: Color(0xFF0A001A), gridLine: Color(0xFF6200EA),
playerRed: Color(0xFFFF007F), playerBlue: Color(0xFF69F0AE), text: Color(0xFFFFFFFF),
);
static const ThemeColors arcade = ThemeColors(
background: Color(0xFF111111), gridLine: Color(0xFF00FF00),
playerRed: Color(0xFFFF004D), playerBlue: Color(0xFF00E5FF), text: Color(0xFFFFFFFF),
);
static const ThemeColors grimorio = ThemeColors(
background: Color(0xFF1E112A), gridLine: Colors.black,
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) {
switch (type) {
case AppThemeType.doodle: return doodle;
case AppThemeType.cyberpunk: return cyberpunk;
case AppThemeType.arcade: return arcade;
case AppThemeType.grimorio: return grimorio;
case AppThemeType.music: return music;
}
}
}
class ThemeIcons {
static IconData gold(AppThemeType type) {
switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.star;
case AppThemeType.cyberpunk: return FontAwesomeIcons.microchip;
case AppThemeType.arcade: return FontAwesomeIcons.coins;
case AppThemeType.grimorio: return FontAwesomeIcons.crown;
case AppThemeType.music: return FontAwesomeIcons.compactDisc;
}
}
static IconData bomb(AppThemeType type) {
switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.virus;
case AppThemeType.cyberpunk: return FontAwesomeIcons.bug;
case AppThemeType.arcade: return FontAwesomeIcons.ghost;
case AppThemeType.grimorio: return FontAwesomeIcons.hatWizard;
case AppThemeType.music: return FontAwesomeIcons.volumeXmark;
}
}
static IconData swap(AppThemeType type) {
switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.arrowsRotate;
case AppThemeType.cyberpunk: return FontAwesomeIcons.networkWired;
case AppThemeType.arcade: return FontAwesomeIcons.shuffle;
case AppThemeType.grimorio: return FontAwesomeIcons.hurricane;
case AppThemeType.music: return FontAwesomeIcons.sliders;
}
}
static IconData joker(AppThemeType type) {
switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.faceSmileBeam;
case AppThemeType.cyberpunk: return FontAwesomeIcons.robot;
case AppThemeType.arcade: return FontAwesomeIcons.gamepad;
case AppThemeType.grimorio: return FontAwesomeIcons.masksTheater;
case AppThemeType.music: return FontAwesomeIcons.headphones;
}
}
static IconData block(AppThemeType type) {
switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.squareXmark;
case AppThemeType.cyberpunk: return FontAwesomeIcons.shieldHalved;
case AppThemeType.arcade: return FontAwesomeIcons.powerOff;
case AppThemeType.grimorio: return FontAwesomeIcons.meteor;
case AppThemeType.music: return FontAwesomeIcons.pause;
}
}
static IconData ice(AppThemeType type) {
if (type == AppThemeType.music) return FontAwesomeIcons.music;
return FontAwesomeIcons.snowflake;
}
static IconData multiplier(AppThemeType type) {
if (type == AppThemeType.music) return FontAwesomeIcons.forwardFast;
return FontAwesomeIcons.bolt;
}
}
// ===========================================================================
// FILE: lib/core/constants.dart
// ===========================================================================
class Constants {
// Chiavi per salvare i dati sul telefono
static const String prefThemeKey = 'selected_theme';
static const String prefLanguageKey = 'selected_language';
static const String prefBoardSizeKey = 'board_size';
// Impostazioni della scacchiera a rombo - RAGGI INCREMENTATI
static const int minBoardRadius = 2; // Ex Normale, ora è Piccola
static const int maxBoardRadius = 5; // Formato MAX, enorme
static const int defaultBoardRadius = 3; // Ora il default è più grande
}
// ===========================================================================
// FILE: lib/core/theme_manager.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/core/theme_manager.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'app_colors.dart';
import '../services/storage_service.dart';
// --- ENUM DEI TEMI AGGIORNATO ---
const Map<AppThemeType, IconData> themeIcons = {
AppThemeType.cyberpunk: Icons.electric_bolt,
AppThemeType.doodle: Icons.brush,
AppThemeType.music: Icons.headset_mic,
AppThemeType.arcade: Icons.videogame_asset,
AppThemeType.grimorio: Icons.auto_stories,
};
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() {
_loadTheme();
}
void _loadTheme() async {
String themeStr = StorageService.instance.getTheme();
AppThemeType loadedType = AppThemeType.values.firstWhere(
(e) => e.toString() == themeStr,
orElse: () => AppThemeType.doodle
);
_currentThemeType = loadedType;
_currentColors = AppColors.getTheme(loadedType);
_updateSystemUI();
notifyListeners();
}
void setTheme(AppThemeType type) {
_currentThemeType = type;
_currentColors = AppColors.getTheme(type);
StorageService.instance.saveTheme(type.toString());
_updateSystemUI();
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,
));
}
}
// ===========================================================================
// FILE: lib/firebase_options.dart
// ===========================================================================
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyBsXO595xVITDPrRnXrW8HPQLOe7Rz4Gg4',
appId: '1:705460445314:android:ceac21bb06b7a9f07b949b',
messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyB77j18Jgeb9gBAEwp-uyOQvr4m-RJ_rAE',
appId: '1:705460445314:ios:54d64cb7592954327b949b',
messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app',
iosBundleId: 'com.amastra.tetraq',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyB77j18Jgeb9gBAEwp-uyOQvr4m-RJ_rAE',
appId: '1:705460445314:ios:da11cbca5d1f6bc27b949b',
messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app',
iosBundleId: 'com.sanza.tetraq',
);
}
// ===========================================================================
// FILE: lib/l10n/app_localizations.dart
// ===========================================================================
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
import 'app_localizations_es.dart';
import 'app_localizations_fr.dart';
import 'app_localizations_it.dart';
import 'app_localizations_pt.dart';
import 'app_localizations_ru.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('de'),
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('it'),
Locale('pt'),
Locale('ru'),
Locale('zh'),
];
/// No description provided for @appTitle.
///
/// In it, this message translates to:
/// **'TetraQ'**
String get appTitle;
/// No description provided for @welcomeTitle.
///
/// In it, this message translates to:
/// **'BENVENUTO IN TETRAQ!'**
String get welcomeTitle;
/// No description provided for @nameHint.
///
/// In it, this message translates to:
/// **'NOME'**
String get nameHint;
/// No description provided for @saveAndPlay.
///
/// In it, this message translates to:
/// **'SALVA E GIOCA'**
String get saveAndPlay;
/// No description provided for @onlineTitle.
///
/// In it, this message translates to:
/// **'ONLINE'**
String get onlineTitle;
/// No description provided for @onlineSub.
///
/// In it, this message translates to:
/// **'Sfida il mondo'**
String get onlineSub;
/// No description provided for @cpuTitle.
///
/// In it, this message translates to:
/// **'VS CPU'**
String get cpuTitle;
/// No description provided for @cpuSub.
///
/// In it, this message translates to:
/// **'Allenati con l\'IA'**
String get cpuSub;
/// No description provided for @localTitle.
///
/// In it, this message translates to:
/// **'LOCALE'**
String get localTitle;
/// No description provided for @localSub.
///
/// In it, this message translates to:
/// **'Stesso schermo'**
String get localSub;
/// No description provided for @leaderboardTitle.
///
/// In it, this message translates to:
/// **'CLASSIFICA'**
String get leaderboardTitle;
/// No description provided for @questsTitle.
///
/// In it, this message translates to:
/// **'SFIDE'**
String get questsTitle;
/// No description provided for @themesTitle.
///
/// In it, this message translates to:
/// **'TEMI'**
String get themesTitle;
/// No description provided for @tutorialTitle.
///
/// In it, this message translates to:
/// **'TUTORIAL'**
String get tutorialTitle;
/// No description provided for @startGame.
///
/// In it, this message translates to:
/// **'AVVIA PARTITA'**
String get startGame;
/// No description provided for @createMatch.
///
/// In it, this message translates to:
/// **'CREA PARTITA'**
String get createMatch;
/// No description provided for @joinMatch.
///
/// In it, this message translates to:
/// **'UNISCITI'**
String get joinMatch;
/// No description provided for @gameOver.
///
/// In it, this message translates to:
/// **'FINE PARTITA'**
String get gameOver;
/// No description provided for @mainMenu.
///
/// In it, this message translates to:
/// **'TORNA AL MENU'**
String get mainMenu;
/// No description provided for @exit.
///
/// In it, this message translates to:
/// **'ESCI'**
String get exit;
}
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.',
);
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_de.dart
// ===========================================================================
// 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';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_en.dart
// ===========================================================================
// 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';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_es.dart
// ===========================================================================
// 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';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_fr.dart
// ===========================================================================
// 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';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_it.dart
// ===========================================================================
// 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';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_pt.dart
// ===========================================================================
// 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';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_ru.dart
// ===========================================================================
// 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 => 'ВЫХОД';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_zh.dart
// ===========================================================================
// 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 => '退出';
}
// ===========================================================================
// FILE: lib/logic/ai_engine.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/logic/ai_engine.dart
// ===========================================================================
import 'dart:math';
import '../models/game_board.dart';
class _ClosureResult {
final bool closesSomething;
final int netValue;
final bool causesSwap;
_ClosureResult(this.closesSomething, this.netValue, this.causesSwap);
}
class AIEngine {
static Line getBestMove(GameBoard board, int level) {
List<Line> availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList();
final random = Random();
if (availableLines.isEmpty) return board.lines.first;
double smartChance = 0.50 + ((level - 1) * 0.10);
if (smartChance > 1.0) smartChance = 1.0;
bool beSmart = random.nextDouble() < smartChance;
int myScore = board.currentPlayer == Player.red ? board.scoreRed : board.scoreBlue;
int oppScore = board.currentPlayer == Player.red ? board.scoreBlue : board.scoreRed;
List<Line> goodClosingMoves = [];
List<Line> badClosingMoves = [];
for (var line in availableLines) {
var result = _checkClosure(board, line);
if (result.closesSomething) {
if (result.causesSwap) {
if (myScore < oppScore) {
goodClosingMoves.add(line);
} else {
badClosingMoves.add(line);
}
} else {
if (result.netValue >= 0) {
goodClosingMoves.add(line);
} else {
badClosingMoves.add(line);
}
}
}
}
// --- REGOLA 1: Chiudere i quadrati vantaggiosi ---
if (goodClosingMoves.isNotEmpty) {
if (beSmart || random.nextDouble() < 0.70) {
return goodClosingMoves[random.nextInt(goodClosingMoves.length)];
}
}
// --- REGOLA 2: Mosse Sicure ---
List<Line> safeMoves = [];
for (var line in availableLines) {
if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && _isSafeMove(board, line, myScore, oppScore)) {
safeMoves.add(line);
}
}
if (safeMoves.isNotEmpty) {
if (beSmart) {
return safeMoves[random.nextInt(safeMoves.length)];
} else {
if (random.nextDouble() < 0.5) {
return safeMoves[random.nextInt(safeMoves.length)];
}
}
}
// --- REGOLA 3: Scegliere il male minore ---
if (beSmart) {
List<Line> riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l)).toList();
if (riskyButNotTerrible.isNotEmpty) {
return riskyButNotTerrible[random.nextInt(riskyButNotTerrible.length)];
}
}
List<Line> nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l)).toList();
if (nonTerribleMoves.isNotEmpty) {
return nonTerribleMoves[random.nextInt(nonTerribleMoves.length)];
}
return availableLines[random.nextInt(availableLines.length)];
}
static _ClosureResult _checkClosure(GameBoard board, Line line) {
int netValue = 0;
bool closesSomething = false;
bool causesSwap = false;
for (var box in board.boxes) {
if (box.type == BoxType.invisible) continue;
if (box.top == line || box.bottom == line || box.left == line || box.right == line) {
int linesCount = 0;
if (box.top.owner != Player.none || box.top == line) linesCount++;
if (box.bottom.owner != Player.none || box.bottom == line) linesCount++;
if (box.left.owner != Player.none || box.left == line) linesCount++;
if (box.right.owner != Player.none || box.right == line) linesCount++;
if (linesCount == 4) {
closesSomething = 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 {
// Se c'è il Jolly del giocatore, l'IA NON DEVE SAPERLO e valuta la casella normalmente!
if (box.type == BoxType.gold) netValue += 2;
else if (box.type == BoxType.bomb) netValue -= 1;
else if (box.type == BoxType.swap) netValue += 0;
else netValue += 1;
}
if (box.type == BoxType.swap) causesSwap = true;
}
}
}
return _ClosureResult(closesSomething, netValue, causesSwap);
}
static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore) {
for (var box in board.boxes) {
if (box.type == BoxType.invisible) continue;
if (box.top == line || box.bottom == line || box.left == line || box.right == line) {
int currentLinesCount = 0;
if (box.top.owner != Player.none) currentLinesCount++;
if (box.bottom.owner != Player.none) currentLinesCount++;
if (box.left.owner != Player.none) currentLinesCount++;
if (box.right.owner != Player.none) currentLinesCount++;
if (currentLinesCount == 2) {
// Nuova logica di sicurezza: cosa succede se l'IA lascia questa scatola all'avversario?
int valueForOpponent = 0;
if (box.hiddenJokerOwner == board.currentPlayer) {
// Se l'avversario la chiude, becca la trappola dell'IA (-1).
// Quindi PER L'IA È SICURISSIMO LASCIARE QUESTA CASELLA APERTA!
valueForOpponent = -1;
} else {
if (box.type == BoxType.gold) valueForOpponent = 2;
else if (box.type == BoxType.bomb) valueForOpponent = -1;
else if (box.type == BoxType.swap) valueForOpponent = 0;
else valueForOpponent = 1;
}
// Se per l'avversario vale -1 (bomba normale o trappola dell'IA), lasciamogliela!
if (valueForOpponent < 0) {
continue;
}
if (box.type == BoxType.swap) {
if (myScore < oppScore) {
continue;
} else {
return false;
}
}
return false;
}
}
}
return true;
}
}
// ===========================================================================
// FILE: lib/logic/game_controller.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/logic/game_controller.dart
// ===========================================================================
import 'dart:async';
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/game_board.dart';
export '../models/game_board.dart';
import 'ai_engine.dart';
import '../services/audio_service.dart';
import '../services/storage_service.dart';
import '../services/multiplayer_service.dart';
import '../core/app_colors.dart';
class CpuMatchSetup {
final int radius;
final ArenaShape shape;
CpuMatchSetup(this.radius, this.shape);
}
class GameController extends ChangeNotifier {
late GameBoard board;
bool isVsCPU = false;
bool isCPUThinking = false;
bool isOnline = false;
String? roomCode;
bool isHost = false;
StreamSubscription<DocumentSnapshot>? _onlineSubscription;
bool opponentLeft = false;
bool _hasSavedResult = false;
Timer? _blitzTimer;
int timeLeft = 15;
int maxTime = 15; // NON PIÙ FINAL! Ora è dinamico in base al livello.
bool isTimeMode = true;
String effectText = '';
Color effectColor = Colors.transparent;
Timer? _effectTimer;
String? myReaction;
String? opponentReaction;
Timer? _myReactionTimer;
Timer? _oppReactionTimer;
Timestamp? _lastOpponentReactionTime;
bool rematchRequested = false;
bool opponentWantsRematch = false;
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 myJokerPlaced = false;
bool oppJokerPlaced = false;
Player jokerTurn = Player.red;
Player get myPlayer => isOnline ? (isHost ? Player.red : Player.blue) : Player.red;
bool get isGameOver => board.isGameOver;
int cpuLevel = 1;
int currentMatchLevel = 1;
int? currentSeed;
AppThemeType _activeTheme = AppThemeType.doodle;
String onlineHostName = "ROSSO";
String onlineGuestName = "BLU";
ArenaShape onlineShape = ArenaShape.classic;
GameController({int radius = 3}) {
cpuLevel = StorageService.instance.cpuLevel;
startNewGame(radius);
}
CpuMatchSetup _getSetupForCpuLevel(int level) {
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, bool timeMode = true}) {
_onlineSubscription?.cancel();
_onlineSubscription = null;
_blitzTimer?.cancel();
_effectTimer?.cancel();
effectText = '';
_hasSavedResult = false;
lastMatchXP = 0;
hasLeveledUp = false;
unlockedRewards.clear();
myReaction = null;
opponentReaction = null;
_lastOpponentReactionTime = null;
rematchRequested = false;
opponentWantsRematch = false;
isSetupPhase = true;
myJokerPlaced = false;
oppJokerPlaced = false;
jokerTurn = Player.red;
this.isVsCPU = vsCPU;
this.isOnline = isOnline;
this.roomCode = roomCode;
this.isHost = isHost;
this.isTimeMode = timeMode;
// --- CALCOLO DEL TIMER DINAMICO ---
int pLevel = StorageService.instance.playerLevel;
// Formula matematica: scala da 15s al Liv.1 fino a 3s al Liv.15
int calculatedTime = 15 - ((pLevel - 1) * 12 / 14).round();
// Assicuriamoci che non scenda mai sotto i 3 secondi e non salga oltre 15
if (calculatedTime < 3) calculatedTime = 3;
if (calculatedTime > 15) calculatedTime = 15;
maxTime = calculatedTime;
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;
board = GameBoard(radius: finalRadius, level: levelToUse, seed: currentSeed, shape: finalShape);
board.currentPlayer = Player.red;
isCPUThinking = false;
opponentLeft = false;
if (this.isOnline && this.roomCode != null) {
_listenToOnlineGame(this.roomCode!);
}
notifyListeners();
}
void placeJoker(int bx, int by) {
if (!isSetupPhase) return;
Box? target;
try { target = board.boxes.firstWhere((b) => b.x == bx && b.y == by); } catch(e) {}
if (target == null || target.type == BoxType.invisible || target.hiddenJokerOwner != null) return;
AudioService.instance.playLineSfx(_activeTheme);
if (isOnline) {
if (myJokerPlaced) return;
target.hiddenJokerOwner = myPlayer;
myJokerPlaced = true;
String prefix = isHost ? 'p1' : 'p2';
FirebaseFirestore.instance.collection('games').doc(roomCode).update({
'${prefix}_joker': {'x': bx, 'y': by}
});
} else {
target.hiddenJokerOwner = jokerTurn;
if (jokerTurn == Player.red) {
jokerTurn = Player.blue;
if (isVsCPU) {
_placeCpuJoker();
}
} else {
jokerTurn = Player.red;
}
}
notifyListeners();
_checkSetupComplete();
}
void _placeCpuJoker() {
var validBoxes = board.boxes.where((b) => b.type != BoxType.invisible && b.hiddenJokerOwner == null).toList();
if (validBoxes.isNotEmpty) {
var b = validBoxes[Random().nextInt(validBoxes.length)];
b.hiddenJokerOwner = Player.blue;
}
jokerTurn = Player.red;
_checkSetupComplete();
}
void _checkSetupComplete() {
if (isOnline) {
if (myJokerPlaced && oppJokerPlaced) {
isSetupPhase = false;
_startTimer();
}
} else {
if (jokerTurn == Player.red) {
isSetupPhase = false;
_startTimer();
}
}
notifyListeners();
}
void sendReaction(String reaction) {
if (!isOnline || roomCode == null) return;
MultiplayerService().sendReaction(roomCode!, isHost, reaction);
_showReaction(true, reaction);
}
void requestRematch() {
if (!isOnline || roomCode == null) return;
rematchRequested = true;
notifyListeners();
MultiplayerService().requestRematch(roomCode!, isHost);
}
void _showReaction(bool isMe, String reaction) {
if (isMe) {
myReaction = reaction;
_myReactionTimer?.cancel();
_myReactionTimer = Timer(const Duration(seconds: 4), () { myReaction = null; notifyListeners(); });
} else {
opponentReaction = reaction;
_oppReactionTimer?.cancel();
_oppReactionTimer = Timer(const Duration(seconds: 4), () { opponentReaction = null; notifyListeners(); });
}
notifyListeners();
}
void triggerSpecialEffect(String text, Color color) {
effectText = text;
effectColor = color;
notifyListeners();
_effectTimer?.cancel();
_effectTimer = Timer(const Duration(milliseconds: 1200), () { effectText = ''; notifyListeners(); });
}
void _playEffects(List<Box> newClosed, {List<Box> newGhosts = const [], required bool isOpponent}) {
if (newGhosts.isNotEmpty) {
AudioService.instance.playBombSfx();
triggerSpecialEffect("TRAPPOLA!", Colors.grey.shade400);
HapticFeedback.heavyImpact();
return;
}
bool isIceCracked = board.lastMove?.isIceCracked ?? false;
if (isIceCracked) {
AudioService.instance.playLineSfx(_activeTheme);
triggerSpecialEffect("GHIACCIO INCRINATO!", Colors.cyanAccent);
HapticFeedback.mediumImpact();
return;
}
if (newClosed.isEmpty) {
AudioService.instance.playLineSfx(_activeTheme);
if (!isOpponent) HapticFeedback.lightImpact();
} else {
for (var b in newClosed) {
if (b.isJokerRevealed) {
if (b.owner == b.hiddenJokerOwner) {
AudioService.instance.playBonusSfx();
triggerSpecialEffect("JOLLY! +2", Colors.greenAccent);
} else {
AudioService.instance.playBombSfx();
triggerSpecialEffect("JOLLY! -1", Colors.redAccent);
}
HapticFeedback.heavyImpact();
return;
}
}
bool isGold = newClosed.any((b) => b.type == BoxType.gold);
bool isBomb = newClosed.any((b) => b.type == BoxType.bomb);
bool isSwap = newClosed.any((b) => b.type == BoxType.swap);
bool isMultiplier = newClosed.any((b) => b.type == BoxType.multiplier);
if (isSwap) {
AudioService.instance.playBonusSfx();
triggerSpecialEffect("SCAMBIO!", Colors.purpleAccent);
HapticFeedback.heavyImpact();
} else if (isMultiplier) {
AudioService.instance.playBonusSfx();
triggerSpecialEffect("MOLTIPLICATORE x2!", Colors.yellowAccent);
HapticFeedback.heavyImpact();
} else if (isGold) {
AudioService.instance.playBonusSfx(); triggerSpecialEffect("+2", Colors.amber); HapticFeedback.heavyImpact();
} else if (isBomb) {
AudioService.instance.playBombSfx(); triggerSpecialEffect("-1", Colors.redAccent); HapticFeedback.heavyImpact();
} else {
AudioService.instance.playBoxSfx(_activeTheme); HapticFeedback.heavyImpact();
}
}
}
void _startTimer() {
_blitzTimer?.cancel();
if (isSetupPhase) return;
timeLeft = maxTime;
if (!isTimeMode) { notifyListeners(); return; }
_blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (isGameOver || isCPUThinking) { timer.cancel(); return; }
if (timeLeft > 0) {
timeLeft--; notifyListeners();
} else {
timer.cancel();
if (!isOnline || board.currentPlayer == myPlayer) { _handleTimeOut(); }
}
});
}
void _handleTimeOut() {
if (!isTimeMode || isSetupPhase) return;
if (isOnline && board.currentPlayer != myPlayer) return;
List<Line> availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList();
if (availableLines.isEmpty) return;
final random = Random();
Line randomMove = availableLines[random.nextInt(availableLines.length)];
handleLineTap(randomMove, _activeTheme, forced: true);
}
void disconnectOnlineGame() {
_onlineSubscription?.cancel();
_onlineSubscription = null;
_blitzTimer?.cancel();
_effectTimer?.cancel();
_myReactionTimer?.cancel();
_oppReactionTimer?.cancel();
_lastOpponentReactionTime = null;
if (isOnline && roomCode != null) {
FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'abandoned'}).catchError((e) => null);
}
isOnline = false; roomCode = null; currentMatchLevel = 1; currentSeed = null;
}
@override
void dispose() { disconnectOnlineGame(); super.dispose(); }
void _listenToOnlineGame(String code) {
_onlineSubscription = FirebaseFirestore.instance.collection('games').doc(code).snapshots().listen((doc) {
if (!doc.exists) return;
var data = doc.data() as Map<String, dynamic>;
onlineHostName = data['hostName'] ?? "ROSSO";
onlineGuestName = (data['guestName'] != null && data['guestName'] != '') ? data['guestName'] : "BLU";
if (data['status'] == 'abandoned' && !board.isGameOver && !opponentLeft) {
opponentLeft = true; notifyListeners(); return;
}
String? p1React = data['p1_reaction'];
Timestamp? p1Time = data['p1_reaction_time'] as Timestamp?;
String? p2React = data['p2_reaction'];
Timestamp? p2Time = data['p2_reaction_time'] as Timestamp?;
if (isHost && p2React != null && p2Time != null && p2Time != _lastOpponentReactionTime) {
_lastOpponentReactionTime = p2Time;
_showReaction(false, p2React);
} else if (!isHost && p1React != null && p1Time != null && p1Time != _lastOpponentReactionTime) {
_lastOpponentReactionTime = p1Time;
_showReaction(false, p1React);
}
bool p1Rematch = data['p1_rematch'] ?? false;
bool p2Rematch = data['p2_rematch'] ?? false;
opponentWantsRematch = isHost ? p2Rematch : p1Rematch;
if (data['status'] == 'playing' && (data['moves'] as List).isEmpty && rematchRequested) {
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++;
int newSeed = DateTime.now().millisecondsSinceEpoch % 1000000;
final rand = Random();
int newRadius = rand.nextInt(4) + 3;
ArenaShape newShape = ArenaShape.values[rand.nextInt(ArenaShape.values.length)];
MultiplayerService().resetMatch(roomCode!, newRadius, newShape.name, newSeed);
}
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();
}
}
List<dynamic> moves = data['moves'] ?? [];
int hostLevel = data['matchLevel'] ?? 1;
int? hostSeed = data['seed'];
int hostRadius = data['radius'] ?? board.radius;
String shapeStr = data['shape'] ?? 'classic';
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
onlineShape = hostShape;
isTimeMode = data['timeMode'] ?? true;
if (!rematchRequested && (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed))) {
currentMatchLevel = hostLevel; currentSeed = hostSeed;
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape);
board.currentPlayer = Player.red;
isCPUThinking = false; notifyListeners(); return;
}
int firebaseMovesCount = moves.length;
int localMovesCount = board.lines.where((l) => l.owner != Player.none).length;
if (firebaseMovesCount == 0 && localMovesCount > 0 && !rematchRequested) {
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape);
board.currentPlayer = Player.red;
notifyListeners(); return;
}
if (firebaseMovesCount > localMovesCount) {
bool newMovesApplied = false;
for (int i = localMovesCount; i < firebaseMovesCount; i++) {
var m = moves[i]; Line? lineToPlay;
for (var line in board.lines) {
if ((line.p1.x == m['x1'] && line.p1.y == m['y1'] && line.p2.x == m['x2'] && line.p2.y == m['y2']) ||
(line.p1.x == m['x2'] && line.p1.y == m['y2'] && line.p2.x == m['x1'] && line.p2.y == m['y1'])) {
lineToPlay = line; break;
}
}
if (lineToPlay != null && lineToPlay.owner == Player.none) {
Player playerFromFirebase = (m['player'] == 'red') ? Player.red : Player.blue;
bool isOpponentMove = (playerFromFirebase != myPlayer);
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
board.playMove(lineToPlay, forcedPlayer: playerFromFirebase);
newMovesApplied = true;
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
List<Box> newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList();
if (isOpponentMove) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: true);
}
}
if (newMovesApplied) {
String expectedTurnStr = data['turn'] ?? 'red';
Player expectedTurn = expectedTurnStr == 'red' ? Player.red : Player.blue;
if (!board.isGameOver && board.currentPlayer != expectedTurn) { board.currentPlayer = expectedTurn; }
_startTimer();
}
if (board.isGameOver) _saveMatchResult();
notifyListeners();
}
});
}
void handleLineTap(Line line, AppThemeType theme, {bool forced = false}) {
if ((isSetupPhase || isCPUThinking || board.isGameOver || opponentLeft) && !forced) return;
if (isOnline && board.currentPlayer != myPlayer && !forced) return;
_activeTheme = theme;
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
if (board.playMove(line)) {
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
List<Box> newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList();
if (!forced) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: false);
_startTimer(); notifyListeners();
if (isOnline && roomCode != null) {
Map<String, dynamic> moveData = {
'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y,
'player': myPlayer == Player.red ? 'red' : 'blue'
};
String nextTurnStr = board.currentPlayer == Player.red ? 'red' : 'blue';
FirebaseFirestore.instance.collection('games').doc(roomCode).update({
'moves': FieldValue.arrayUnion([moveData]),
'turn': nextTurnStr
}).catchError((e) => debugPrint("Errore: $e"));
if (board.isGameOver) {
_saveMatchResult();
if (isHost) FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'finished'});
}
} else {
if (board.isGameOver) _saveMatchResult();
else if (isVsCPU && board.currentPlayer == Player.blue) _checkCPUTurn();
}
}
}
void _checkCPUTurn() async {
if (isVsCPU && board.currentPlayer == Player.blue && !board.isGameOver) {
isCPUThinking = true; _blitzTimer?.cancel(); notifyListeners();
await Future.delayed(const Duration(milliseconds: 600));
if (!board.isGameOver) {
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
Line bestMove = AIEngine.getBestMove(board, cpuLevel);
board.playMove(bestMove);
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
List<Box> newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList();
_playEffects(newClosed, newGhosts: newGhosts, isOpponent: true);
isCPUThinking = false; _startTimer(); notifyListeners();
if (board.isGameOver) _saveMatchResult();
else _checkCPUTurn();
}
}
}
List<Map<String, dynamic>> _getUnlocks(int oldLevel, int newLevel) {
List<Map<String, dynamic>> unlocks = [];
for(int i = oldLevel + 1; i <= newLevel; i++) {
if (rewardsRoadmap.containsKey(i)) {
unlocks.addAll(rewardsRoadmap[i]!);
}
}
return unlocks;
}
void _saveMatchResult() {
if (_hasSavedResult) return;
_hasSavedResult = true;
int calculatedXP = 0;
bool isDraw = board.scoreRed == board.scoreBlue;
String myRealName = StorageService.instance.playerName;
if (myRealName.isEmpty) myRealName = "IO";
int oldLevel = StorageService.instance.playerLevel;
if (isOnline) {
bool isWin = isHost ? board.scoreRed > board.scoreBlue : board.scoreBlue > board.scoreRed;
calculatedXP = isWin ? 20 : (isDraw ? 5 : 2);
String oppName = isHost ? onlineGuestName : onlineHostName;
int myScore = isHost ? board.scoreRed : board.scoreBlue;
int oppScore = isHost ? board.scoreBlue : board.scoreRed;
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
if (isWin) StorageService.instance.updateQuestProgress(0, 1);
} else if (isVsCPU) {
int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
bool isWin = myScore > cpuScore;
calculatedXP = isWin ? (10 + (cpuLevel * 2)) : (isDraw ? 5 : 2);
if (isWin) {
StorageService.instance.addWin();
StorageService.instance.updateQuestProgress(1, 1);
} else if (cpuScore > myScore) {
StorageService.instance.addLoss();
}
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false);
} else {
calculatedXP = 2;
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false);
}
if (board.shape != ArenaShape.classic) {
StorageService.instance.updateQuestProgress(2, 1);
}
lastMatchXP = calculatedXP;
StorageService.instance.addXP(calculatedXP);
int newLevel = StorageService.instance.playerLevel;
if (newLevel > oldLevel) {
hasLeveledUp = true;
newlyReachedLevel = newLevel;
unlockedRewards = _getUnlocks(oldLevel, newLevel);
}
notifyListeners();
}
void increaseLevelAndRestart() {
cpuLevel++; StorageService.instance.saveCpuLevel(cpuLevel);
startNewGame(board.radius, vsCPU: true, shape: board.shape, timeMode: isTimeMode);
}
}
// ===========================================================================
// FILE: lib/main.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/main.dart
// ===========================================================================
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart';
import 'core/theme_manager.dart';
import 'logic/game_controller.dart';
import 'ui/home/home_screen.dart';
import 'services/storage_service.dart';
import 'services/audio_service.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'firebase_options.dart';
import 'package:firebase_app_check/firebase_app_check.dart';
// --- NUOVI IMPORT PER GLI AGGIORNAMENTI ---
import 'package:upgrader/upgrader.dart';
import 'package:in_app_update/in_app_update.dart';
// --- IMPORT PER IL SUPPORTO MULTILINGUA ---
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:tetraq/l10n/app_localizations.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
await FirebaseAppCheck.instance.activate(
androidProvider: kDebugMode ? AndroidProvider.debug : AndroidProvider.playIntegrity,
appleProvider: kDebugMode ? AppleProvider.debug : AppleProvider.deviceCheck,
);
try {
await FirebaseAuth.instance.signInAnonymously();
} catch (e) {
debugPrint("Errore Auth: $e");
}
await StorageService.instance.init();
await AudioService.instance.init();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeManager()),
ChangeNotifierProvider(create: (_) => GameController()),
],
child: const TetraQApp(),
),
);
}
class TetraQApp extends StatelessWidget {
const TetraQApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'TetraQ',
debugShowCheckedModeBanner: false,
theme: ThemeData(
fontFamily: 'Roboto',
useMaterial3: true,
),
// --- BIVIO DELLE LINGUE ATTIVATO! ---
// Flutter si occuperà di caricare automaticamente tutte le lingue
// che hai generato tramite lo script.
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
// ------------------------------------
// Avvolgiamo la HomeScreen nel nostro nuovo gestore di aggiornamenti!
home: const 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();
// Controlla gli aggiornamenti in background solo se siamo su Android
if (!kIsWeb && Platform.isAndroid) {
_checkForAndroidUpdate();
}
}
Future<void> _checkForAndroidUpdate() async {
try {
final info = await InAppUpdate.checkForUpdate();
if (info.updateAvailability == UpdateAvailability.updateAvailable) {
// Se possibile, fai scaricare l'aggiornamento in background mentre l'utente gioca
if (info.flexibleUpdateAllowed) {
await InAppUpdate.startFlexibleUpdate();
await InAppUpdate.completeFlexibleUpdate(); // Chiede il riavvio rapido dell'app
}
// Se l'aggiornamento è impostato come critico dalla console di Google Play
else if (info.immediateUpdateAllowed) {
await InAppUpdate.performImmediateUpdate();
}
}
} catch (e) {
debugPrint("Errore in_app_update Android: $e");
}
}
@override
Widget build(BuildContext context) {
// Su iOS e macOS usiamo "upgrader" che si occupa di mostrare il pop-up nativo
if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) {
return UpgradeAlert(
dialogStyle: (Platform.isIOS || Platform.isMacOS)
? UpgradeDialogStyle.cupertino
: UpgradeDialogStyle.material,
showIgnore: false, // <-- Spostato qui
showLater: true, // <-- Spostato qui
upgrader: Upgrader(
// debugDisplayAlways: true, // <--- Scommenta questa riga se vuoi testare la UI del pop-up sul Mac ora!
),
child: widget.child,
);
}
// Su Android restituiamo la UI normale (l'aggiornamento è gestito nel background da initState)
return widget.child;
}
}
// ===========================================================================
// FILE: lib/models/game_board.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/models/game_board.dart
// ===========================================================================
import 'dart:math';
enum Player { red, blue, none }
enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } // Aggiunti ice e multiplier
enum ArenaShape { classic, cross, donut, hourglass, chaos }
class Dot {
final int x;
final int y;
Dot(this.x, this.y);
@override
bool operator ==(Object other) => identical(this, other) || other is Dot && runtimeType == other.runtimeType && x == other.x && y == other.y;
@override
int get hashCode => x.hashCode ^ y.hashCode;
}
class Line {
final Dot p1;
final Dot p2;
Player owner = Player.none;
bool isPlayable = false;
bool isIceCracked = false; // NUOVO: Stato per il blocco di ghiaccio
Line(this.p1, this.p2);
bool connects(Dot a, Dot b) { return (p1 == a && p2 == b) || (p1 == b && p2 == a); }
}
class Box {
final int x;
final int y;
Player owner = Player.none;
late Line top, bottom, left, right;
BoxType type = BoxType.normal;
bool isRevealed = false;
Player? hiddenJokerOwner;
bool isJokerRevealed = false;
Box(this.x, this.y);
bool isClosed() {
if (type == BoxType.invisible) return false;
return top.owner != Player.none && bottom.owner != Player.none && left.owner != Player.none && right.owner != Player.none;
}
int getCalculatedValue(Player closer) {
if (hiddenJokerOwner != null) {
return (closer == hiddenJokerOwner) ? 2 : -1;
}
if (type == BoxType.gold) return 2;
if (type == BoxType.bomb) return -1;
if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; // Il moltiplicatore e il ghiaccio non danno punti base
return 1;
}
}
class GameBoard {
final int radius;
final int level;
final int? seed;
final ArenaShape shape;
late int columns;
late int rows;
List<Dot> dots = [];
List<Line> lines = [];
List<Box> boxes = [];
Player currentPlayer = Player.red;
int scoreRed = 0;
int scoreBlue = 0;
bool isGameOver = false;
Line? lastMove;
// Variabili per il Moltiplicatore
bool redHasMultiplier = false;
bool blueHasMultiplier = false;
GameBoard({required this.radius, this.level = 1, this.seed, this.shape = ArenaShape.classic}) {
_generateBoard();
}
void _generateBoard() {
final random = seed != null ? Random(seed) : Random();
int chaosAlgorithm = random.nextInt(5);
if (shape == ArenaShape.chaos) {
columns = radius * 2 + 1;
rows = (radius * 3) + 2;
} else {
columns = radius * 2 + 1;
rows = radius * 2 + 1;
}
dots.clear();
lines.clear();
boxes.clear();
lastMove = null;
for (int y = 0; y < rows; y++) {
for (int x = 0; x < columns; x++) {
var box = Box(x, y);
bool isVisible = true;
if (shape != ArenaShape.chaos) {
int dx = (x - radius).abs();
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; // Nuova Scatola Ghiaccio
else if (level >= 15 && chance > 0.78 && chance <= 0.83) box.type = BoxType.multiplier; // Nuova Scatola x2
}
boxes.add(box);
}
}
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) {
for (var dot in dots) { if (dot.x == x && dot.y == y) return dot; }
var newDot = Dot(x, y);
dots.add(newDot); return newDot;
}
Line _getOrAddLine(Dot a, Dot b) {
for (var line in lines) { if (line.connects(a, b)) return line; }
var newLine = Line(a, b);
lines.add(newLine); return newLine;
}
bool playMove(Line lineToPlay, {Player? forcedPlayer}) {
if (isGameOver) return false;
Player playerMakingMove = forcedPlayer ?? currentPlayer;
Line? actualLine;
for (var l in lines) {
if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; }
}
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; // Si incrina ma non si chiude!
lastMove = actualLine;
if (forcedPlayer == null) currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red;
else currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red;
return true; // Mossa valida, ma turno finito.
}
// Mossa normale o secondo colpo al ghiaccio
actualLine.isIceCracked = false;
actualLine.owner = playerMakingMove;
lastMove = actualLine;
bool scoredPoint = false;
bool triggeredSwap = false;
for (var box in boxes) {
if (box.owner == Player.none && box.isClosed()) {
box.owner = playerMakingMove;
scoredPoint = true;
if (box.hiddenJokerOwner != null) box.isJokerRevealed = true;
int points = box.getCalculatedValue(playerMakingMove);
// --- LOGICA MOLTIPLICATORE x2 ---
if (box.type == BoxType.multiplier) {
if (playerMakingMove == Player.red) redHasMultiplier = true;
else blueHasMultiplier = true;
} else if (points != 0) {
// Se la scatola chiusa dà punti e il giocatore ha un x2 attivo...
if (playerMakingMove == Player.red && redHasMultiplier) {
points *= 2;
redHasMultiplier = false; // Si consuma
} else if (playerMakingMove == Player.blue && blueHasMultiplier) {
points *= 2;
blueHasMultiplier = false; // Si consuma
}
}
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) {
int temp = scoreRed; scoreRed = scoreBlue; scoreBlue = temp;
}
if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) { isGameOver = true; }
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;
}
}
// ===========================================================================
// FILE: lib/models/player_info.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/services/audio_service.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/services/audio_service.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import '../core/app_colors.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AudioService extends ChangeNotifier {
static final AudioService instance = AudioService._internal();
AudioService._internal();
bool isMuted = false;
final AudioPlayer _sfxPlayer = AudioPlayer();
final AudioPlayer _bgmPlayer = AudioPlayer();
AppThemeType _currentTheme = AppThemeType.doodle;
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
isMuted = prefs.getBool('isMuted') ?? false;
await _bgmPlayer.setReleaseMode(ReleaseMode.loop);
}
void toggleMute() async {
isMuted = !isMuted;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isMuted', isMuted);
if (isMuted) {
await _bgmPlayer.pause();
await _sfxPlayer.stop();
} else {
playBgm(_currentTheme);
}
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 {
if (isMuted) return;
String file = '';
switch (theme) {
case AppThemeType.arcade:
case AppThemeType.music:
file = 'minimal_line.wav'; break;
case AppThemeType.doodle:
file = 'doodle_line.wav'; break;
case AppThemeType.cyberpunk:
case AppThemeType.grimorio:
file = 'cyber_line.wav'; break;
}
if (file.isNotEmpty) {
try {
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
} catch (e) {
debugPrint("Errore SFX Linea: $file");
}
}
}
void playBoxSfx(AppThemeType theme) async {
if (isMuted) return;
String file = '';
switch (theme) {
case AppThemeType.arcade:
case AppThemeType.music:
file = 'minimal_box.wav'; break;
case AppThemeType.doodle:
file = 'doodle_box.wav'; break;
case AppThemeType.cyberpunk:
case AppThemeType.grimorio:
file = 'cyber_box.wav'; break;
}
if (file.isNotEmpty) {
try {
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
} catch (e) {
debugPrint("Errore SFX Box: $file");
}
}
}
void playBonusSfx() async {
if (isMuted) return;
try {
await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0);
} catch(e) {}
}
void playBombSfx() async {
if (isMuted) return;
try {
await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0);
} catch(e) {}
}
}
// ===========================================================================
// FILE: lib/services/firebase_service.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/services/multiplayer_service.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/services/multiplayer_service.dart
// ===========================================================================
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:share_plus/share_plus.dart';
class MultiplayerService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
CollectionReference get _gamesCollection => _firestore.collection('games');
CollectionReference get _invitesCollection => _firestore.collection('invites');
Future<String> createGameRoom(int boardRadius, String hostName, String shapeName, bool isTimeMode, {bool isPublic = true}) async {
String roomCode = _generateRoomCode();
int randomSeed = Random().nextInt(1000000);
await _gamesCollection.doc(roomCode).set({
'status': 'waiting',
'radius': boardRadius,
'createdAt': FieldValue.serverTimestamp(),
'players': ['host'],
'turn': 'host',
'moves': [],
'seed': randomSeed,
'hostName': hostName,
'hostUid': _auth.currentUser?.uid,
'guestName': '',
'shape': shapeName,
'timeMode': isTimeMode,
'isPublic': isPublic,
'p1_reaction': null,
'p2_reaction': null,
'p1_rematch': false,
'p2_rematch': false,
});
return roomCode;
}
Future<Map<String, dynamic>?> joinGameRoom(String roomCode, String guestName) async {
DocumentSnapshot doc = await _gamesCollection.doc(roomCode).get();
if (doc.exists && doc['status'] == 'waiting') {
await _gamesCollection.doc(roomCode).update({
'status': 'playing',
'players': FieldValue.arrayUnion(['guest']),
'guestName': guestName,
});
return doc.data() as Map<String, dynamic>;
}
return null;
}
Stream<QuerySnapshot> getPublicRooms() {
return _gamesCollection
.where('status', isEqualTo: 'waiting')
.where('isPublic', isEqualTo: true)
.snapshots();
}
void shareInviteLink(String roomCode) {
// ECCO IL TUO SMART LINK FIREBASE!
String smartLink = "https://tetraq-32a4a.web.app";
String message = "Ehi! Giochiamo a TetraQ? 🎮\n\n"
"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);
}
Stream<DocumentSnapshot> listenToRoom(String roomCode) {
return _gamesCollection.doc(roomCode).snapshots();
}
String _generateRoomCode() {
const chars = 'ACDEFGHJKLMNPQRSTUVWXYZ2345679';
final random = Random();
return String.fromCharCodes(Iterable.generate(
5, (_) => chars.codeUnitAt(random.nextInt(chars.length)),
));
}
Future<void> sendReaction(String roomCode, bool isHost, String reaction) async {
try {
String prefix = isHost ? 'p1' : 'p2';
await _gamesCollection.doc(roomCode).update({
'${prefix}_reaction': reaction,
'${prefix}_reaction_time': FieldValue.serverTimestamp(),
});
} catch (e) {
debugPrint("Errore invio reazione: $e");
}
}
Future<void> requestRematch(String roomCode, bool isHost) async {
try {
String prefix = isHost ? 'p1' : 'p2';
await _gamesCollection.doc(roomCode).update({
'${prefix}_rematch': true,
});
} catch (e) {
debugPrint("Errore richiesta rivincita: $e");
}
}
Future<void> resetMatch(String roomCode, int newRadius, String newShape, int newSeed) async {
try {
await _gamesCollection.doc(roomCode).update({
'status': 'playing',
'moves': [],
'seed': newSeed,
'radius': newRadius,
'shape': newShape,
'p1_rematch': false,
'p2_rematch': false,
'p1_reaction': null,
'p2_reaction': null,
});
} catch (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");
}
}
}
// ===========================================================================
// FILE: lib/services/storage_service.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/services/storage_service.dart
// ===========================================================================
import 'dart:convert';
import 'dart:io' show Platform, HttpClient;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cloud_firestore/cloud_firestore.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 {
static final StorageService instance = StorageService._internal();
StorageService._internal();
late SharedPreferences _prefs;
int _sessionStart = 0;
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
_checkDailyQuests();
_fetchLocationData();
_sessionStart = DateTime.now().millisecondsSinceEpoch;
}
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;
Future<void> saveRadius(int radius) async => await _prefs.setInt('radius', radius);
bool get isMuted => _prefs.getBool('isMuted') ?? false;
Future<void> saveMuted(bool muted) async => await _prefs.setBool('isMuted', muted);
int get totalXP => _prefs.getInt('totalXP') ?? 0;
Future<void> addXP(int xp) async {
await _prefs.setInt('totalXP', totalXP + xp);
syncLeaderboard();
}
int get playerLevel => (totalXP / 100).floor() + 1;
int get wins => _prefs.getInt('wins') ?? 0;
Future<void> addWin() async {
await _prefs.setInt('wins', wins + 1);
syncLeaderboard();
}
int get losses => _prefs.getInt('losses') ?? 0;
Future<void> addLoss() async => await _prefs.setInt('losses', losses + 1);
int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1;
Future<void> saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level);
String get playerName => _prefs.getString('playerName') ?? '';
Future<void> savePlayerName(String name) async {
await _prefs.setString('playerName', name);
syncLeaderboard();
}
// --- SINCRONIZZAZIONE BLINDATA: SOLO UTENTI REGISTRATI ---
Future<void> syncLeaderboard() async {
try {
final user = FirebaseAuth.instance.currentUser;
// BLOCCO TOTALE: Se non sei loggato con la password, niente database!
if (user == null) return;
String name = playerName;
if (name.isEmpty) name = "GIOCATORE"; // Fallback di sicurezza
String targetUid = user.uid;
await FirebaseFirestore.instance.collection('leaderboard').doc(targetUid).set({
'name': name,
'xp': totalXP,
'level': playerLevel,
'wins': wins,
'losses': losses,
'lastActive': FieldValue.serverTimestamp(),
}, SetOptions(merge: true));
} catch (e) {
debugPrint("Errore durante la sincronizzazione della classifica: $e");
}
}
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() {
String today = DateTime.now().toIso8601String().substring(0, 10);
String lastDate = _prefs.getString('quest_date') ?? '';
if (today != lastDate) {
_prefs.setString('quest_date', today);
_prefs.setInt('q1_type', 0);
_prefs.setInt('q1_prog', 0);
_prefs.setInt('q1_target', 3);
_prefs.setInt('q2_type', 1);
_prefs.setInt('q2_prog', 0);
_prefs.setInt('q2_target', 2);
_prefs.setInt('q3_type', 2);
_prefs.setInt('q3_prog', 0);
_prefs.setInt('q3_target', 2);
}
}
Future<void> updateQuestProgress(int type, int amount) async {
for(int i=1; i<=3; i++) {
if (_prefs.getInt('q${i}_type') == type) {
int prog = _prefs.getInt('q${i}_prog') ?? 0;
int target = _prefs.getInt('q${i}_target') ?? 1;
if (prog < target) {
_prefs.setInt('q${i}_prog', prog + amount);
}
}
}
}
List<Map<String, dynamic>> get matchHistory {
List<String> history = _prefs.getStringList('matchHistory') ?? [];
return history.map((e) => jsonDecode(e) as Map<String, dynamic>).toList();
}
Future<void> saveMatchToHistory({required String myName, required String opponent, required int myScore, required int oppScore, required bool isOnline}) async {
List<String> history = _prefs.getStringList('matchHistory') ?? [];
Map<String, dynamic> match = {
'date': DateTime.now().toIso8601String(),
'myName': myName, 'opponent': opponent, 'myScore': myScore, 'oppScore': oppScore, 'isOnline': isOnline,
};
history.insert(0, jsonEncode(match));
if (history.length > 50) history = history.sublist(0, 50);
await _prefs.setStringList('matchHistory', history);
}
}
// ===========================================================================
// FILE: lib/ui/admin/admin_screen.dart
// ===========================================================================
// ===========================================================================
// 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')}";
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').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(),
),
// QUI È DOVE AVVENIVA IL CRASH! Ora usiamo Expanded e FittedBox
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))),
],
),
),
],
)
],
),
),
);
},
);
}
),
);
}
}
// ===========================================================================
// FILE: lib/ui/game/board_painter.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/ui/game/board_painter.dart
// ===========================================================================
import 'dart:math';
import 'package:flutter/material.dart';
import '../../models/game_board.dart';
import '../../core/app_colors.dart';
class BoardPainter extends CustomPainter {
final GameBoard board;
final ThemeColors theme;
final AppThemeType themeType;
final double blinkValue;
final bool isOnline;
final bool isVsCPU;
final bool isSetupPhase;
final Player myPlayer;
final Player jokerTurn;
BoardPainter({
required this.board,
required this.theme,
required this.themeType,
required this.isOnline,
required this.isVsCPU,
required this.isSetupPhase,
required this.myPlayer,
required this.jokerTurn,
this.blinkValue = 0.0
});
@override
void paint(Canvas canvas, Size size) {
if (themeType == AppThemeType.doodle) {
final Paint paperGridPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..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);
}
}
int gridPoints = board.columns + 1;
double spacing = size.width / gridPoints;
double offset = spacing / 2;
Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset);
// =======================================================================
// 1. CREAZIONE DELLA SAGOMA DELL'ARENA (SFONDO E BORDO)
// =======================================================================
Path arenaShape = Path();
bool isFirst = true;
// Uniamo la forma di ogni box giocabile per creare un'unica sagoma
for (var box in board.boxes) {
if (box.type != BoxType.invisible) { // Ignora i buchi
Offset p1 = getScreenPos(box.x, box.y);
Offset p2 = getScreenPos(box.x + 1, box.y + 1);
Path boxPath = Path()..addRect(Rect.fromPoints(p1, p2));
if (isFirst) {
arenaShape = boxPath;
isFirst = false;
} else {
arenaShape = Path.combine(PathOperation.union, arenaShape, boxPath);
}
}
}
// --- DISEGNO DELLO SFONDO LUMINOSO ---
final fillPaint = Paint()
..style = PaintingStyle.fill
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10.0);
if (themeType == AppThemeType.music) {
fillPaint.color = Colors.white.withOpacity(0.08);
canvas.drawPath(arenaShape, fillPaint);
} else if (themeType == AppThemeType.cyberpunk) {
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));
}
}
}
if (box.type == BoxType.gold) {
_drawIconInBox(canvas, rect, ThemeIcons.gold(themeType), Colors.amber);
} else if (box.type == BoxType.bomb) {
_drawIconInBox(canvas, rect, ThemeIcons.bomb(themeType), themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.greenAccent : Colors.deepPurple);
} else if (box.type == BoxType.swap) {
_drawIconInBox(canvas, rect, ThemeIcons.swap(themeType), Colors.purpleAccent);
} else if (box.type == BoxType.ice) {
_drawIconInBox(canvas, rect, ThemeIcons.ice(themeType), Colors.cyanAccent);
} else if (box.type == BoxType.multiplier) {
_drawIconInBox(canvas, rect, ThemeIcons.multiplier(themeType), Colors.yellowAccent);
}
}
for (var line in board.lines) {
if (!line.isPlayable) continue;
Offset p1 = getScreenPos(line.p1.x, line.p1.y);
Offset p2 = getScreenPos(line.p2.x, line.p2.y);
// --- DISEGNO DELLA LINEA "INCRINATA" DAL GHIACCIO ---
if (line.isIceCracked) {
_drawCrackedIceLine(canvas, p1, p2, blinkValue);
continue;
}
bool isLastMove = (line == board.lastMove);
Color lineColor = line.owner == Player.none
? theme.gridLine.withOpacity(0.4)
: (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
if (isLastMove && line.owner != Player.none && themeType != AppThemeType.cyberpunk && themeType != AppThemeType.arcade && themeType != AppThemeType.grimorio) {
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.5)..strokeWidth = 16.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0));
}
if (themeType == AppThemeType.cyberpunk) {
_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;
Set<Dot> activeDots = {};
for (var line in board.lines) {
if (line.isPlayable) {
activeDots.add(line.p1); activeDots.add(line.p2);
}
}
for (var dot in activeDots) {
Offset pos = getScreenPos(dot.x, dot.y);
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) {
TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr);
textPainter.text = TextSpan(
text: String.fromCharCode(icon.codePoint),
style: TextStyle(
color: themeType == AppThemeType.arcade ? color : color.withOpacity(0.7),
fontSize: rect.width * 0.45,
fontFamily: icon.fontFamily,
package: icon.fontPackage,
shadows: themeType == AppThemeType.arcade ? [] : [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))]
),
);
textPainter.layout();
textPainter.paint(canvas, Offset(rect.center.dx - textPainter.width / 2, rect.center.dy - textPainter.height / 2));
}
void _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) {
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);
canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0);
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy);
double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);
Path crack = Path()..moveTo(p1.dx, p1.dy);
int zigzags = 6;
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);
crack.lineTo(basePt.dx + perp.x * offset, basePt.dy + perp.y * offset);
}
crack.lineTo(p2.dx, p2.dy);
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;
}
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); }
}
// ===========================================================================
// FILE: lib/ui/game/game_screen.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/ui/game/game_screen.dart
// ===========================================================================
import 'dart:ui';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../logic/game_controller.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
import '../../models/game_board.dart';
import 'board_painter.dart';
import 'score_board.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../services/storage_service.dart';
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));
} else if (themeType == AppThemeType.music) {
return GoogleFonts.audiowide(textStyle: baseStyle.copyWith(letterSpacing: 1.5));
}
return baseStyle;
}
class GameScreen extends StatefulWidget {
const GameScreen({super.key});
@override
State<GameScreen> createState() => _GameScreenState();
}
class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
late AnimationController _blinkController;
bool _gameOverDialogShown = false;
bool _opponentLeftDialogShown = false;
bool _hideJokerMessage = false;
bool _wasSetupPhase = false;
Player _lastJokerTurn = Player.red;
@override
void initState() {
super.initState();
_blinkController = AnimationController(vsync: this, duration: const Duration(milliseconds: 600))..repeat(reverse: true);
}
@override
void dispose() { _blinkController.dispose(); super.dispose(); }
void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) {
_gameOverDialogShown = true;
showDialog(
barrierDismissible: false,
context: context,
builder: (dialogContext) => Consumer<GameController>(
builder: (context, controller, child) {
if (!controller.isGameOver) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_gameOverDialogShown) {
_gameOverDialogShown = false;
if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext);
}
});
return const SizedBox.shrink();
}
int red = controller.board.scoreRed; int blue = controller.board.scoreBlue;
bool playerBeatCPU = controller.isVsCPU && red > blue;
String myName = StorageService.instance.playerName.toUpperCase();
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";
String winnerText = ""; Color winnerColor = theme.text;
if (red > blue) { winnerText = "VINCE $nameRed!"; winnerColor = theme.playerRed; }
else if (blue > red) { winnerText = "VINCE $nameBlue!"; winnerColor = theme.playerBlue; }
else { winnerText = "PAREGGIO!"; winnerColor = theme.text; }
return AlertDialog(
backgroundColor: theme.background,
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))),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(winnerText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
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) ...[
const SizedBox(height: 15),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
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 || 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))),
),
],
if (controller.isVsCPU) ...[
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)))),
],
if (controller.isOnline) ...[
const SizedBox(height: 20),
if (controller.rematchRequested && !controller.opponentWantsRematch)
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.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),
actionsAlignment: MainAxisAlignment.center,
actions: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (playerBeatCPU)
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))),
)
else if (controller.isOnline)
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))),
)
else
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))),
),
const SizedBox(height: 12),
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))),
),
],
)
],
);
}
)
);
}
Widget _buildThemedJokerMessage(ThemeColors theme, AppThemeType themeType, GameController gameController) {
String titleText = "";
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 || 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(
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 || themeType == AppThemeType.music ? 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
)),
),
],
),
);
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
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final themeType = themeManager.currentThemeType;
final theme = themeManager.currentColors;
final gameController = context.watch<GameController>();
if (gameController.isSetupPhase && !_wasSetupPhase) {
_hideJokerMessage = false;
_lastJokerTurn = Player.red;
} else if (gameController.isSetupPhase && gameController.jokerTurn != _lastJokerTurn) {
_hideJokerMessage = false;
_lastJokerTurn = gameController.jokerTurn;
}
_wasSetupPhase = gameController.isSetupPhase;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (gameController.opponentLeft && !_opponentLeftDialogShown) {
_opponentLeftDialogShown = true;
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))),
)
],
)
);
} else if (gameController.board.isGameOver && !_gameOverDialogShown) {
_showGameOverDialog(context, gameController, theme, themeType);
}
});
String? bgImage;
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;
Widget emojiBar = const SizedBox();
if (gameController.isOnline && !gameController.isGameOver) {
final List<String> emojis = ['😂', '😡', '😱', '🥳', '👀'];
emojiBar = Container(
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(
child: Stack(
children: [
Column(
children: [
const ScoreBoard(),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 2.0),
child: LayoutBuilder(
builder: (context, constraints) {
int cols = gameController.board.columns + 1;
int rows = gameController.board.rows + 1;
double boxSize = constraints.maxWidth / cols;
double requiredHeight = boxSize * rows;
if (requiredHeight > constraints.maxHeight) { boxSize = constraints.maxHeight / rows; }
double actualWidth = boxSize * cols;
double actualHeight = boxSize * rows;
return SizedBox(
width: actualWidth, height: actualHeight,
child: Stack(
children: [
Positioned.fill(
child: ClipPath(
clipper: _ArenaClipper(gameController.board),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
child: Container(
color: themeType == AppThemeType.doodle
? Colors.black.withOpacity(0.05)
: Colors.white.withOpacity(0.12),
),
),
),
),
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,
),
);
}
),
),
],
),
);
}
),
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 10.0, left: 20.0, right: 20.0, top: 5.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (gameController.isVsCPU)
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))),
],
),
)
else
emojiBar,
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))),
),
),
],
),
)
],
),
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!)),
],
),
);
return PopScope(
canPop: true,
onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); },
child: Scaffold(
backgroundColor: themeType == AppThemeType.doodle ? Colors.white : (bgImage != null ? Colors.transparent : theme.background),
body: Stack(
children: [
Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background),
if (themeType == AppThemeType.doodle)
Positioned.fill(
child: CustomPaint(
painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)),
),
),
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))),
],
),
),
);
}
void _handleTap(Offset tapPos, double width, double height, GameController controller, AppThemeType themeType) {
final board = controller.board;
if (board.isGameOver) return;
int cols = board.columns + 1; double spacing = width / cols; double offset = spacing / 2;
if (controller.isSetupPhase) {
int bx = ((tapPos.dx - offset) / spacing).floor(); int by = ((tapPos.dy - offset) / spacing).floor();
controller.placeJoker(bx, by); return;
}
Line? closestLine; double minDistance = double.infinity; double maxTouchDistance = spacing * 0.4;
for (var line in board.lines) {
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);
double dist = _distanceToSegment(tapPos, screenP1, screenP2);
if (dist < minDistance && dist < maxTouchDistance) { minDistance = dist; closestLine = line; }
}
if (closestLine != null) { controller.handleLineTap(closestLine, themeType); }
}
double _distanceToSegment(Offset p, Offset a, Offset b) {
double l2 = (a.dx - b.dx) * (a.dx - b.dx) + (a.dy - b.dy) * (a.dy - b.dy);
if (l2 == 0) return (p - a).distance;
double t = (((p.dx - a.dx) * (b.dx - a.dx) + (p.dy - a.dy) * (b.dy - a.dy)) / l2).clamp(0.0, 1.0);
Offset projection = Offset(a.dx + t * (b.dx - a.dx), a.dy + t * (b.dy - a.dy));
return (p - projection).distance;
}
}
// ===========================================================================
// 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 {
double x, y, vx, vy, size, angle, spin;
Color color; int type;
_Particle({required this.x, required this.y, required this.vx, required this.vy, required this.color, required this.size, required this.angle, required this.spin, required this.type});
}
class WinnerVFXOverlay extends StatefulWidget {
final Color winnerColor; final AppThemeType themeType;
const WinnerVFXOverlay({super.key, required this.winnerColor, required this.themeType});
@override State<WinnerVFXOverlay> createState() => _WinnerVFXOverlayState();
}
class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerProviderStateMixin {
late AnimationController _vfxController;
final List<_Particle> _particles = [];
final math.Random _rand = math.Random();
bool _initialized = false;
@override
void initState() {
super.initState();
_vfxController = AnimationController(vsync: this, duration: const Duration(seconds: 4))..addListener(() { _updateParticles(); })..forward();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_initialized) { _initParticles(MediaQuery.of(context).size); _initialized = true; }
}
void _initParticles(Size screenSize) {
int particleCount = widget.themeType == AppThemeType.cyberpunk || widget.themeType == AppThemeType.music ? 150 : 100;
if (widget.themeType == AppThemeType.arcade) particleCount = 80;
if (widget.themeType == AppThemeType.grimorio) particleCount = 120;
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); }
else if (widget.themeType == AppThemeType.doodle) { palette.add(const Color(0xFF00008B)); palette.add(Colors.redAccent); }
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.music) { palette.add(Colors.pinkAccent); palette.add(Colors.cyanAccent); }
for (int i = 0; i < particleCount; i++) {
double speed = _rand.nextDouble() * 20 + 5;
double theta = _rand.nextDouble() * 2 * math.pi;
_particles.add(_Particle(x: screenSize.width / 2, y: screenSize.height / 2, vx: speed * math.cos(theta), vy: speed * math.sin(theta) - 5, color: palette[_rand.nextInt(palette.length)], size: _rand.nextDouble() * 10 + 6, angle: _rand.nextDouble() * math.pi, spin: (_rand.nextDouble() - 0.5) * 0.5, type: _rand.nextInt(3)));
}
}
void _updateParticles() {
setState(() {
for (var p in _particles) {
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; }
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 { p.vy += 0.5; }
p.angle += p.spin; p.size *= 0.99;
}
});
}
@override void dispose() { _vfxController.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return CustomPaint(painter: _VFXPainter(particles: _particles, themeType: widget.themeType), child: Container()); }
}
class _VFXPainter extends CustomPainter {
final List<_Particle> particles; final AppThemeType themeType;
_VFXPainter({required this.particles, required this.themeType});
@override
void paint(Canvas canvas, Size size) {
for (var p in particles) {
if (p.size < 0.5) continue;
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); }
canvas.save(); canvas.translate(p.x, p.y); canvas.rotate(p.angle);
if (themeType == AppThemeType.doodle) {
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); }
} else if (themeType == AppThemeType.arcade) {
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 1.5, height: p.size * 1.5), paint);
} else if (themeType == AppThemeType.grimorio) {
paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0);
canvas.drawCircle(Offset.zero, p.size, paint);
canvas.drawCircle(Offset.zero, p.size * 0.3, Paint()..color=Colors.white..style=PaintingStyle.fill);
} else {
if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); }
else if (p.type == 1) { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 2, height: p.size * 2), paint); }
else { var path = Path()..moveTo(0, -p.size)..lineTo(p.size, p.size)..lineTo(-p.size, p.size)..close(); canvas.drawPath(path, paint); }
}
canvas.restore();
}
}
@override bool shouldRepaint(covariant _VFXPainter oldDelegate) => true;
}
class _BouncingEmoji extends StatefulWidget {
final String emoji; const _BouncingEmoji({required this.emoji});
@override State<_BouncingEmoji> createState() => _BouncingEmojiState();
}
class _BouncingEmojiState extends State<_BouncingEmoji> with SingleTickerProviderStateMixin {
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 dispose() { _ctrl.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return AnimatedBuilder(animation: _anim, builder: (ctx, child) => Transform.translate(offset: Offset(0, _anim.value), child: Container(padding: const EdgeInsets.all(8), decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5)]), child: Text(widget.emoji, style: const TextStyle(fontSize: 32))))); }
}
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;
}
class BlitzBackgroundEffect extends StatefulWidget {
final int timeLeft; final Color color; final AppThemeType themeType;
const BlitzBackgroundEffect({super.key, required this.timeLeft, required this.color, required this.themeType});
@override State<BlitzBackgroundEffect> createState() => _BlitzBackgroundEffectState();
}
class _BlitzBackgroundEffectState extends State<BlitzBackgroundEffect> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true); }
@override void dispose() { _controller.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return AnimatedBuilder(animation: _controller, builder: (context, child) { return Container(color: widget.color.withOpacity(0.12 * _controller.value), child: Center(child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0), child: Text('${widget.timeLeft}', style: _getTextStyle(widget.themeType, TextStyle(fontSize: 300, fontWeight: FontWeight.w900, color: widget.color.withOpacity(0.35 + (0.3 * _controller.value)), height: 1.0)))))); }); }
}
class SpecialEventBackgroundEffect extends StatefulWidget {
final String text; final Color color; final AppThemeType themeType;
const SpecialEventBackgroundEffect({super.key, required this.text, required this.color, required this.themeType});
@override State<SpecialEventBackgroundEffect> createState() => _SpecialEventBackgroundEffectState();
}
class _SpecialEventBackgroundEffectState extends State<SpecialEventBackgroundEffect> with SingleTickerProviderStateMixin {
late AnimationController _controller; late Animation<double> _scaleAnimation; late Animation<double> _opacityAnimation;
@override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward(); _scaleAnimation = Tween<double>(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); _opacityAnimation = Tween<double>(begin: 0.9, end: 0.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); }
@override void didUpdateWidget(covariant SpecialEventBackgroundEffect oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.text != widget.text) { _controller.reset(); _controller.forward(); } }
@override void dispose() { _controller.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return AnimatedBuilder(animation: _controller, builder: (context, child) { return Center(child: Transform.scale(scale: _scaleAnimation.value, child: Opacity(opacity: _opacityAnimation.value, child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), child: Text(widget.text, textAlign: TextAlign.center, style: _getTextStyle(widget.themeType, TextStyle(fontSize: 150, fontWeight: FontWeight.w900, color: widget.color, height: 1.0))))))); }); }
}
// ===========================================================================
// FILE: lib/ui/game/score_board.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/ui/game/score_board.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../logic/game_controller.dart';
import '../../models/game_board.dart';
import '../../core/theme_manager.dart';
import '../../services/audio_service.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 {
const ScoreBoard({super.key});
@override
State<ScoreBoard> createState() => _ScoreBoardState();
}
class _ScoreBoardState extends State<ScoreBoard> {
@override
Widget build(BuildContext context) {
final controller = context.watch<GameController>();
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
int redScore = controller.board.scoreRed;
int blueScore = controller.board.scoreBlue;
bool isRedTurn = controller.board.currentPlayer == Player.red;
bool isMuted = AudioService.instance.isMuted;
String myName = StorageService.instance.playerName.toUpperCase();
if (myName.isEmpty) myName = "TU";
String nameRed = myName;
String nameBlue = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU";
if (controller.isOnline) {
nameRed = controller.onlineHostName.toUpperCase();
nameBlue = controller.onlineGuestName.toUpperCase();
} else if (controller.isVsCPU) {
nameRed = myName;
nameBlue = "CPU";
}
return Container(
padding: const EdgeInsets.only(top: 10, bottom: 20, left: 20, right: 20),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? theme.background : theme.background.withOpacity(0.95),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
offset: const Offset(0, 4),
blurRadius: 8,
),
],
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_PlayerScore(color: theme.playerRed, score: redScore, isTurn: isRedTurn, textColor: theme.text, title: nameRed, themeType: themeType),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"TETRAQ",
style: _getTextStyle(themeType, TextStyle(
fontSize: 24,
fontWeight: FontWeight.w900,
color: theme.text,
letterSpacing: 4,
shadows: themeType == AppThemeType.doodle
? [
// 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),
// --- ROW DEI PULSANTI AGGIORNATA ---
Row(
mainAxisSize: MainAxisSize.min,
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),
],
),
);
}
}
class _PlayerScore extends StatelessWidget {
final Color color;
final int score;
final bool isTurn;
final Color textColor;
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});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: isTurn ? color : textColor.withOpacity(0.5), fontSize: 12))),
const SizedBox(height: 5),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
decoration: BoxDecoration(
color: color.withOpacity(isTurn ? 1.0 : 0.2),
borderRadius: BorderRadius.circular(15),
border: isTurn ? Border.all(color: Colors.white.withOpacity(0.4), width: 2) : Border.all(color: Colors.transparent, width: 2),
boxShadow: isTurn ? [
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)))),
),
],
);
}
}
// ===========================================================================
// FILE: lib/ui/home/dialog.dart
// ===========================================================================
// ===========================================================================
// 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; // <-- Aggiunto Callback per inviare i dati alla HomeScreen
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();
return name != 'PAOLO';
}).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);
// Chiama la funzione passata dalla HomeScreen!
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))
),
],
),
),
),
);
}
}
// ===========================================================================
// FILE: lib/ui/home/history_screen.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../../core/theme_manager.dart';
import '../../services/storage_service.dart';
class HistoryScreen extends StatelessWidget {
const HistoryScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = context.watch<ThemeManager>().currentColors;
final history = StorageService.instance.matchHistory;
return Scaffold(
backgroundColor: theme.background,
appBar: AppBar(
title: Text("STORICO PARTITE", style: TextStyle(fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2)),
backgroundColor: Colors.transparent,
elevation: 0,
iconTheme: IconThemeData(color: theme.text),
),
body: history.isEmpty
? Center(
child: Text(
"Nessuna partita giocata.\nScendi in campo!",
textAlign: TextAlign.center,
style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 18, fontWeight: FontWeight.bold),
),
)
: ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: history.length,
itemBuilder: (context, index) {
final match = history[index];
DateTime date = DateTime.parse(match['date']);
String formattedDate = DateFormat('dd MMM yyyy - HH:mm').format(date);
// Leggiamo entrambi i nomi
String myName = match['myName'] ?? "IO"; // Usa 'IO' se è una partita vecchia
String opponent = match['opponent'];
int myScore = match['myScore'];
int oppScore = match['oppScore'];
bool isOnline = match['isOnline'];
bool isWin = myScore > oppScore;
bool isDraw = myScore == oppScore;
Color resultColor = isWin ? Colors.green : (isDraw ? Colors.grey : theme.playerRed);
String resultText = isWin ? "VITTORIA" : (isDraw ? "PAREGGIO" : "SCONFITTA");
return Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: resultColor.withOpacity(0.5), width: 2),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.2), offset: const Offset(0, 4), blurRadius: 6),
],
),
child: Row(
children: [
// Icona Tipo di Partita
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: resultColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
isOnline ? Icons.public : (opponent.contains("CPU") ? Icons.smart_toy : Icons.people_alt),
color: resultColor,
size: 28,
),
),
const SizedBox(width: 15),
// Dati Partita (Ora con i nomi chiari)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(resultText, style: TextStyle(color: resultColor, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5)),
const SizedBox(height: 5),
// NOMI GIOCATORI
RichText(
text: TextSpan(
children: [
TextSpan(text: myName, style: TextStyle(color: theme.playerBlue, fontWeight: FontWeight.bold, fontSize: 15)),
TextSpan(text: " vs ", style: TextStyle(color: theme.text.withOpacity(0.5), fontStyle: FontStyle.italic, fontSize: 12)),
TextSpan(text: opponent, style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 15)),
]
)
),
const SizedBox(height: 5),
Text(formattedDate, style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 12)),
],
),
),
// Punteggio
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: theme.background,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: theme.gridLine.withOpacity(0.3)),
),
child: Row(
children: [
Text("$myScore", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.playerBlue)),
Text(" - ", style: TextStyle(fontSize: 18, color: theme.text.withOpacity(0.5))),
Text("$oppScore", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.playerRed)),
],
),
),
],
),
);
},
),
);
}
}
// ===========================================================================
// FILE: lib/ui/home/home_modals.dart
// ===========================================================================
// ===========================================================================
// 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_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";
try {
if (isLogin) {
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 {
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);
if (context.mounted) Navigator.of(dialogContext).pop();
onSuccess();
} on FirebaseAuthException catch (e) {
String msg = "Errore di autenticazione.";
if (e.code == 'email-already-in-use') msg = "Nome occupato!\nSe è il tuo account, clicca su ACCEDI.";
else if (e.code == 'user-not-found' || e.code == 'wrong-password' || e.code == 'invalid-credential') msg = "Nome o Password errati!";
setStateDialog(() { errorMessage = msg; isLoadingAuth = false; });
} catch (e) {
setStateDialog(() { errorMessage = "Errore imprevisto: $e"; isLoadingAuth = false; });
}
}
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)
);
},
);
},
);
}
// --- NUOVO DIALOGO ESCLUSIVO PER LA SFIDA ONLINE DIRETTA ---
static void showChallengeSetupDialog(BuildContext context, String targetName, Function(int radius, ArenaShape shape, bool timeMode) onStart) {
int localRadius = 4; ArenaShape localShape = ArenaShape.classic; bool localTimeMode = true;
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", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10),
NeonTimeSwitch(isTimeMode: localTimeMode, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localTimeMode = !localTimeMode)), 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", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
NeonTimeSwitch(isTimeMode: localTimeMode, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localTimeMode = !localTimeMode)), 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);
},
);
}
);
}
// --- DIALOGO NORMALE VS CPU / LOCALE ---
static void showMatchSetupDialog(BuildContext context, bool isVsCPU) {
int localRadius = 4; ArenaShape localShape = ArenaShape.classic; bool localTimeMode = true;
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 dimensioni 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),
NeonTimeSwitch(isTimeMode: localTimeMode, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localTimeMode = !localTimeMode)), 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 dimensioni 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),
NeonTimeSwitch(isTimeMode: localTimeMode, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localTimeMode = !localTimeMode)), 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 bool isTimeMode,
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: isTimeMode);
// LA CORREZIONE È QUI: Usiamo push invece di pushReplacement
Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen()));
});
}
}
return PopScope(
canPop: false,
onPopInvoked: (didPop) {
if (didPop) return;
onCleanup();
Navigator.pop(ctx);
},
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
dialogContent,
const SizedBox(height: 20),
TextButton(
onPressed: () {
onCleanup();
Navigator.pop(ctx);
},
child: Text("ANNULLA", style: getSharedTextStyle(themeType, TextStyle(color: Colors.red, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 2.0, shadows: themeType == AppThemeType.doodle ? [] : [const Shadow(color: Colors.black, blurRadius: 2)]))),
),
],
),
),
);
},
);
}
);
}
static void showJoinPromptDialog(BuildContext context, String roomCode, Function(String) onConfirm) {
showDialog(
context: context,
builder: (context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
return AlertDialog(
backgroundColor: themeType == AppThemeType.doodle ? Colors.white : theme.background,
shape: themeType == AppThemeType.doodle ? RoundedRectangleBorder(borderRadius: BorderRadius.circular(15), side: BorderSide(color: theme.text, width: 2)) : null,
title: Text("Invito Trovato!", style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold))),
content: Text("Vuoi unirti alla stanza $roomCode?", style: getSharedTextStyle(themeType, TextStyle(color: theme.text))),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text("No", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.red)))),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: themeType == AppThemeType.doodle ? Colors.transparent : theme.playerBlue, elevation: 0, side: themeType == AppThemeType.doodle ? BorderSide(color: theme.text, width: 1.5) : BorderSide.none),
onPressed: () {
Navigator.of(context).pop();
onConfirm(roomCode);
},
child: Text(AppLocalizations.of(context)!.joinMatch, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : Colors.white, fontWeight: FontWeight.bold))),
),
],
);
}
);
}
static void showFavoritesDialog(BuildContext context, Function(String, String) onInvite) {
final favs = StorageService.instance.favorites;
showDialog(
context: context,
builder: (ctx) {
final themeManager = ctx.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
return AlertDialog(
backgroundColor: theme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text("I TUOI PREFERITI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold))),
content: Container(
width: double.maxFinite,
height: 300,
decoration: BoxDecoration(
border: Border.all(color: theme.playerRed, width: 2),
borderRadius: BorderRadius.circular(10)
),
child: favs.isEmpty
? Center(child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text("Non hai ancora aggiunto nessun preferito dalla Classifica!", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6)))),
))
: ListView.builder(
itemCount: favs.length,
itemBuilder: (c, i) {
return ListTile(
title: Text(favs[i]['name']!, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontSize: 18, fontWeight: FontWeight.bold))),
trailing: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
onPressed: () {
Navigator.pop(ctx);
onInvite(favs[i]['uid']!, favs[i]['name']!);
},
child: Text("SFIDA", style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
),
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text("CHIUDI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))),
),
],
);
}
);
}
}
// ===========================================================================
// FILE: lib/ui/home/home_screen.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/ui/home/home_screen.dart
// ===========================================================================
import 'dart:ui';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'dart:async';
import 'package:app_links/app_links.dart';
import '../../logic/game_controller.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
import '../../services/storage_service.dart';
import '../../services/audio_service.dart';
import '../../services/multiplayer_service.dart';
import '../multiplayer/lobby_screen.dart';
import '../admin/admin_screen.dart';
import '../settings/settings_screen.dart';
import '../game/game_screen.dart';
import 'package:tetraq/l10n/app_localizations.dart';
import '../../widgets/painters.dart';
import '../../widgets/cyber_border.dart';
import '../../widgets/music_theme_widgets.dart';
import '../../widgets/home_buttons.dart';
import 'dialog.dart';
import 'home_modals.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
int _debugTapCount = 0;
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
StreamSubscription<QuerySnapshot>? _favoritesSubscription;
StreamSubscription<QuerySnapshot>? _invitesSubscription;
Map<String, DateTime> _lastOnlineNotifications = {};
final int _selectedRadius = 4;
final ArenaShape _selectedShape = ArenaShape.classic;
final bool _isTimeMode = true;
final bool _isPublicRoom = true;
bool _isLoading = false;
String? _myRoomCode;
bool _roomStarted = false;
final MultiplayerService _multiplayerService = MultiplayerService();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (FirebaseAuth.instance.currentUser == null) {
HomeModals.showNameDialog(context, () {
StorageService.instance.syncLeaderboard();
_listenToInvites();
setState(() {});
});
} else {
StorageService.instance.syncLeaderboard();
_listenToInvites();
}
_checkThemeSafety();
});
_checkClipboardForInvite();
_initDeepLinks();
_listenToFavoritesOnline();
}
void _checkThemeSafety() {
String themeStr = StorageService.instance.getTheme();
bool exists = AppThemeType.values.any((e) => e.toString() == themeStr);
if (!exists) {
context.read<ThemeManager>().setTheme(AppThemeType.doodle);
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_cleanupGhostRoom();
_linkSubscription?.cancel();
_favoritesSubscription?.cancel();
_invitesSubscription?.cancel();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_checkClipboardForInvite();
_listenToFavoritesOnline();
} else if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) {
_cleanupGhostRoom();
}
}
void _cleanupGhostRoom() {
if (_myRoomCode != null && !_roomStarted) {
FirebaseFirestore.instance.collection('games').doc(_myRoomCode).delete();
_myRoomCode = null;
}
}
Future<void> _initDeepLinks() async {
_appLinks = AppLinks();
try {
final initialUri = await _appLinks.getInitialLink();
if (initialUri != null) _handleDeepLink(initialUri);
} catch (e) { debugPrint("Errore lettura link iniziale: $e"); }
_linkSubscription = _appLinks.uriLinkStream.listen((uri) { _handleDeepLink(uri); }, onError: (err) { debugPrint("Errore stream link: $err"); });
}
void _handleDeepLink(Uri uri) {
if (uri.scheme == 'tetraq' && uri.host == 'join') {
String? code = uri.queryParameters['code'];
if (code != null && code.length == 5) {
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) HomeModals.showJoinPromptDialog(context, code.toUpperCase(), _joinRoomByCode);
});
}
}
}
Future<void> _checkClipboardForInvite() async {
try {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
String? text = data?.text;
if (text != null && text.contains("TetraQ") && text.contains("codice:")) {
RegExp regExp = RegExp(r'codice:\s*([A-Z0-9]{5})', caseSensitive: false);
Match? match = regExp.firstMatch(text);
if (match != null) {
String roomCode = match.group(1)!.toUpperCase();
await Clipboard.setData(const ClipboardData(text: ''));
if (mounted && ModalRoute.of(context)?.isCurrent == true) {
HomeModals.showJoinPromptDialog(context, roomCode, _joinRoomByCode);
}
}
}
} catch (e) { debugPrint("Errore lettura appunti: $e"); }
}
void _listenToFavoritesOnline() {
_favoritesSubscription?.cancel();
final favs = StorageService.instance.favorites;
if (favs.isEmpty) return;
List<String> favUids = favs.map((f) => f['uid']!).toList();
if (favUids.length > 10) favUids = favUids.sublist(0, 10);
_favoritesSubscription = FirebaseFirestore.instance
.collection('leaderboard')
.where(FieldPath.documentId, whereIn: favUids)
.snapshots()
.listen((snapshot) {
if (!mounted) return;
for (var change in snapshot.docChanges) {
if (change.type == DocumentChangeType.modified || change.type == DocumentChangeType.added) {
var data = change.doc.data();
if (data != null && data['lastActive'] != null) {
Timestamp lastActive = data['lastActive'];
int diffInSeconds = DateTime.now().difference(lastActive.toDate()).inSeconds;
if (diffInSeconds.abs() < 180) {
String name = data['name'] ?? 'Un amico';
_showFavoriteOnlinePopup(name);
}
}
}
}
});
}
void _showFavoriteOnlinePopup(String name) {
if (!mounted) return;
if (_lastOnlineNotifications.containsKey(name)) {
if (DateTime.now().difference(_lastOnlineNotifications[name]!).inMinutes < 1) return;
}
_lastOnlineNotifications[name] = DateTime.now();
final overlay = Overlay.of(context);
late OverlayEntry entry;
bool removed = false;
entry = OverlayEntry(
builder: (context) => Positioned(
top: MediaQuery.of(context).padding.top + 15,
left: 20,
right: 20,
child: FavoriteOnlinePopup(
name: name,
onDismiss: () {
if (!removed) {
removed = true;
entry.remove();
}
},
),
),
);
overlay.insert(entry);
}
void _listenToInvites() {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
_invitesSubscription?.cancel();
_invitesSubscription = FirebaseFirestore.instance
.collection('invites')
.where('toUid', isEqualTo: user.uid)
.snapshots()
.listen((snapshot) {
if (!mounted) return;
for (var change in snapshot.docChanges) {
if (change.type == DocumentChangeType.added) {
var data = change.doc.data();
if (data != null) {
String code = data['roomCode'];
String from = data['fromName'];
String inviteId = change.doc.id;
Timestamp? ts = data['timestamp'];
if (ts != null) {
if (DateTime.now().difference(ts.toDate()).inMinutes > 2) {
FirebaseFirestore.instance.collection('invites').doc(inviteId).delete();
continue;
}
}
_showInvitePopup(from, code, inviteId);
}
}
}
});
}
void _showInvitePopup(String fromName, String roomCode, String inviteId) {
final themeType = context.read<ThemeManager>().currentThemeType;
final theme = context.read<ThemeManager>().currentColors;
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
backgroundColor: themeType == AppThemeType.doodle ? Colors.white : 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),
Text("SFIDA IN ARRIVO!", style: getSharedTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 18))),
],
),
content: Text("$fromName ti ha sfidato a duello!\nAccetti la sfida?", style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))),
actions: [
TextButton(
onPressed: () {
FirebaseFirestore.instance.collection('invites').doc(inviteId).delete();
Navigator.pop(ctx);
},
child: Text("RIFIUTA", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.grey, fontWeight: FontWeight.bold))),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
onPressed: () {
FirebaseFirestore.instance.collection('invites').doc(inviteId).delete();
Navigator.pop(ctx);
_joinRoomByCode(roomCode);
},
child: Text("ACCETTA!", style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))),
),
],
)
);
}
// --- 1. FUNZIONE PONTE CHE APRE IL POPUP DI SETUP ---
void _startDirectChallengeFlow(String targetUid, String targetName) {
HomeModals.showChallengeSetupDialog(
context,
targetName,
(int radius, ArenaShape shape, bool timeMode) {
// Quando l'utente clicca AVVIA nel setup, lanciamo la sfida reale
_executeSendChallenge(targetUid, targetName, radius, shape, timeMode);
}
);
}
// --- 2. FUNZIONE REALE CHE CREA LA STANZA E MANDA L'INVITO ---
Future<void> _executeSendChallenge(String targetUid, String targetName, int radius, ArenaShape shape, bool timeMode) async {
setState(() => _isLoading = true);
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
final rnd = Random();
String roomCode = String.fromCharCodes(Iterable.generate(5, (_) => chars.codeUnitAt(rnd.nextInt(chars.length))));
try {
int gameSeed = rnd.nextInt(9999999);
await FirebaseFirestore.instance.collection('games').doc(roomCode).set({
'status': 'waiting',
'hostName': StorageService.instance.playerName,
'hostUid': FirebaseAuth.instance.currentUser?.uid,
'radius': radius, // <--- Usiamo la misura scelta!
'shape': shape.name, // <--- Usiamo la forma scelta!
'timeMode': timeMode, // <--- Usiamo il tempo scelto!
'isPublic': false,
'createdAt': FieldValue.serverTimestamp(),
'players': [FirebaseAuth.instance.currentUser?.uid],
'turn': 0,
'moves': [],
'seed': gameSeed,
});
await FirebaseFirestore.instance.collection('invites').add({
'toUid': targetUid,
'fromName': StorageService.instance.playerName,
'roomCode': roomCode,
'timestamp': FieldValue.serverTimestamp(),
});
setState(() => _isLoading = false);
if (mounted) {
HomeModals.showWaitingDialog(
context: context,
code: roomCode,
isPublicRoom: false,
selectedRadius: radius,
selectedShape: shape,
isTimeMode: timeMode,
multiplayerService: _multiplayerService,
onRoomStarted: () {},
onCleanup: () {
FirebaseFirestore.instance.collection('games').doc(roomCode).delete();
}
);
}
} catch (e) {
setState(() => _isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore: $e", style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red));
}
}
}
Future<void> _joinRoomByCode(String code) async {
if (_isLoading) return;
FocusScope.of(context).unfocus();
setState(() => _isLoading = true);
try {
String playerName = StorageService.instance.playerName;
if (playerName.isEmpty) playerName = "GUEST";
Map<String, dynamic>? roomData = await _multiplayerService.joinGameRoom(code, playerName);
if (!mounted) return;
setState(() => _isLoading = false);
if (roomData != null) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("La sfida inizierà a breve..."), backgroundColor: Colors.green));
int hostRadius = roomData['radius'] ?? 4;
String shapeStr = roomData['shape'] ?? 'classic';
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
bool hostTimeMode = roomData['timeMode'] ?? true;
context.read<GameController>().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode);
// LA CORREZIONE È QUI: Usiamo push invece di pushReplacement
Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen()));
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza non trovata, piena o partita già iniziata.", style: TextStyle(color: Colors.white)), backgroundColor: Colors.red));
}
} catch (e) {
if (mounted) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore di connessione: $e", style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red));
}
}
}
BoxDecoration _glassBoxDecoration(ThemeColors theme, AppThemeType themeType) {
return BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : null,
gradient: themeType == AppThemeType.doodle ? null : LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(0.25),
Colors.white.withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(25),
border: Border.all(
color: themeType == AppThemeType.doodle ? theme.text : Colors.white.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), blurRadius: 0)]
: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 10)],
);
}
Widget _buildTopBar(BuildContext context, ThemeColors theme, AppThemeType themeType, String playerName, int playerLevel) {
Color inkColor = const Color(0xFF111122);
return Padding(
padding: const EdgeInsets.only(top: 5.0, left: 15.0, right: 15.0, bottom: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => HomeModals.showNameDialog(context, () => setState(() {})),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: _glassBoxDecoration(theme, themeType),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 18,
backgroundColor: theme.playerBlue.withOpacity(0.2),
child: Icon(Icons.person, color: theme.playerBlue, size: 20),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
playerName.toUpperCase(),
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontWeight: FontWeight.bold, fontSize: 16)),
overflow: TextOverflow.visible,
softWrap: false,
),
Text(
"LIV. $playerLevel",
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.8) : theme.playerBlue, fontWeight: FontWeight.bold, fontSize: 11)),
overflow: TextOverflow.visible,
softWrap: false,
),
],
),
],
),
),
),
// --- BOX STATISTICHE ---
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: _glassBoxDecoration(theme, themeType),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(themeType == AppThemeType.music ? FontAwesomeIcons.microphone : Icons.emoji_events, color: Colors.amber.shade600, size: 16),
const SizedBox(width: 6),
Text(
"${StorageService.instance.wins}",
style: getSharedTextStyle(themeType, TextStyle(
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
fontWeight: FontWeight.w900,
fontSize: 16,
)),
overflow: TextOverflow.visible,
softWrap: false,
),
const SizedBox(width: 10),
Icon(themeType == AppThemeType.music ? FontAwesomeIcons.compactDisc : Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 16),
const SizedBox(width: 6),
Text(
"${StorageService.instance.losses}",
style: getSharedTextStyle(themeType, TextStyle(
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
fontWeight: FontWeight.w900,
fontSize: 16,
)),
overflow: TextOverflow.visible,
softWrap: false,
),
const SizedBox(width: 10),
Container(width: 1, height: 20, color: (themeType == AppThemeType.doodle ? inkColor : Colors.white).withOpacity(0.2)),
const SizedBox(width: 10),
AnimatedBuilder(
animation: AudioService.instance,
builder: (context, child) {
bool isMuted = AudioService.instance.isMuted;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
AudioService.instance.toggleMute();
},
child: Icon(
isMuted ? Icons.volume_off : Icons.volume_up,
color: isMuted ? theme.playerRed : (themeType == AppThemeType.doodle ? inkColor : theme.text),
size: 20,
),
);
}
),
],
),
),
],
),
);
}
Widget _buildCyberCard(Widget card, AppThemeType themeType) {
if (themeType == AppThemeType.cyberpunk) return AnimatedCyberBorder(child: card);
return card;
}
@override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
final themeManager = context.watch<ThemeManager>();
final themeType = themeManager.currentThemeType;
final theme = themeManager.currentColors;
Color inkColor = const Color(0xFF111122);
final loc = AppLocalizations.of(context)!;
String? bgImage;
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';
String playerName = StorageService.instance.playerName;
if (playerName.isEmpty) playerName = "GUEST";
int playerLevel = StorageService.instance.playerLevel;
final double screenHeight = MediaQuery.of(context).size.height;
final double vScale = (screenHeight / 920.0).clamp(0.50, 1.0);
Widget uiContent = SafeArea(
child: Column(
children: [
_buildTopBar(context, theme, themeType, playerName, playerLevel),
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 20 * vScale),
Center(
child: Transform.rotate(
angle: themeType == AppThemeType.doodle ? -0.04 : 0,
child: GestureDetector(
onTap: () {
if (playerName.toUpperCase() == 'PAOLO') {
_debugTapCount++;
if (_debugTapCount == 5) {
StorageService.instance.addXP(2000);
setState(() {});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("🛠 DEBUG MODE: +20 Livelli!", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), backgroundColor: Colors.purpleAccent, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)))
);
} else if (_debugTapCount >= 7) {
_debugTapCount = 0;
Navigator.push(context, MaterialPageRoute(builder: (_) => AdminScreen()));
}
}
},
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
loc.appTitle.toUpperCase(),
style: getSharedTextStyle(themeType, TextStyle(
fontSize: 65 * vScale,
fontWeight: FontWeight.w900,
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
letterSpacing: 10 * vScale,
shadows: themeType == AppThemeType.doodle
? [const Shadow(color: Colors.white, offset: Offset(2.5, 2.5), blurRadius: 2), const Shadow(color: Colors.white, offset: Offset(-2.5, -2.5), blurRadius: 2)]
: [Shadow(color: Colors.black.withOpacity(0.8), offset: const Offset(3, 4), blurRadius: 8), Shadow(color: theme.playerBlue.withOpacity(0.4), offset: const Offset(0, 0), blurRadius: 20)]
)),
overflow: TextOverflow.visible,
softWrap: false,
),
),
),
),
),
SizedBox(height: 40 * vScale),
if (themeType == AppThemeType.music) ...[
MusicCassetteCard(title: loc.onlineTitle, subtitle: loc.onlineSub, neonColor: Colors.blueAccent, angle: -0.04, leftIcon: FontAwesomeIcons.sliders, rightIcon: FontAwesomeIcons.globe, themeType: themeType, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => LobbyScreen())); }),
SizedBox(height: 12 * vScale),
MusicCassetteCard(title: loc.cpuTitle, subtitle: loc.cpuSub, neonColor: Colors.purpleAccent, angle: 0.03, leftIcon: FontAwesomeIcons.desktop, rightIcon: FontAwesomeIcons.music, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, true)),
SizedBox(height: 12 * vScale),
MusicCassetteCard(title: loc.localTitle, subtitle: loc.localSub, neonColor: Colors.deepPurpleAccent, angle: -0.02, leftIcon: FontAwesomeIcons.headphones, rightIcon: FontAwesomeIcons.headphones, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, false)),
SizedBox(height: 30 * vScale),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: MusicKnobCard(title: loc.leaderboardTitle, icon: FontAwesomeIcons.compactDisc, iconColor: Colors.amber, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _startDirectChallengeFlow)))), // <--- ORA CHIAMA LA FUNZIONE PONTE
Expanded(child: MusicKnobCard(title: loc.questsTitle, icon: FontAwesomeIcons.microphoneLines, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()))),
Expanded(child: MusicKnobCard(title: loc.themesTitle, icon: FontAwesomeIcons.palette, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen())))),
Expanded(child: MusicKnobCard(title: loc.tutorialTitle, icon: FontAwesomeIcons.bookOpen, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const TutorialDialog()))),
],
),
] else ...[
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildCyberCard(FeatureCard(title: loc.onlineTitle, subtitle: loc.onlineSub, icon: Icons.public, color: Colors.lightBlue.shade200, theme: theme, themeType: themeType, isFeatured: true, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => LobbyScreen())); }), themeType),
SizedBox(height: 12 * vScale),
_buildCyberCard(FeatureCard(title: loc.cpuTitle, subtitle: loc.cpuSub, icon: Icons.smart_toy, color: Colors.purple.shade200, theme: theme, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, true)), themeType),
SizedBox(height: 12 * vScale),
_buildCyberCard(FeatureCard(title: loc.localTitle, subtitle: loc.localSub, icon: Icons.people_alt, color: Colors.red.shade200, theme: theme, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, false)), themeType),
SizedBox(height: 12 * vScale),
Row(
children: [
Expanded(child: _buildCyberCard(FeatureCard(title: loc.leaderboardTitle, subtitle: "Top 50 Globale", icon: Icons.leaderboard, color: Colors.amber.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _startDirectChallengeFlow)), compact: true), themeType)), // <--- ORA CHIAMA LA FUNZIONE PONTE
const SizedBox(width: 12),
Expanded(child: _buildCyberCard(FeatureCard(title: loc.questsTitle, subtitle: "Missioni", icon: Icons.assignment_turned_in, color: Colors.green.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()), compact: true), themeType)),
],
),
SizedBox(height: 12 * vScale),
Row(
children: [
Expanded(child: _buildCyberCard(FeatureCard(title: loc.themesTitle, subtitle: "Personalizza", icon: Icons.palette, color: Colors.teal.shade200, theme: theme, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen())), compact: true), themeType)),
const SizedBox(width: 12),
Expanded(child: _buildCyberCard(FeatureCard(title: loc.tutorialTitle, subtitle: "Come giocare", icon: Icons.school, color: Colors.indigo.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const TutorialDialog()), compact: true), themeType)),
],
),
],
),
],
SizedBox(height: 40 * vScale),
],
),
),
),
),
],
),
);
return Scaffold(
backgroundColor: bgImage != null ? Colors.transparent : theme.background,
extendBodyBehindAppBar: true,
body: Stack(
children: [
Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background),
if (themeType == AppThemeType.doodle)
Positioned.fill(
child: CustomPaint(
painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)),
),
),
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 (themeType == AppThemeType.music)
Positioned.fill(
child: IgnorePointer(
child: CustomPaint(
painter: AudioCablesPainter(),
),
),
),
Positioned.fill(child: uiContent),
],
),
);
}
}
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;
}
class FavoriteOnlinePopup extends StatefulWidget {
final String name;
final VoidCallback onDismiss;
const FavoriteOnlinePopup({super.key, required this.name, required this.onDismiss});
@override
State<FavoriteOnlinePopup> createState() => _FavoriteOnlinePopupState();
}
class _FavoriteOnlinePopupState extends State<FavoriteOnlinePopup> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _offsetAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400));
_offsetAnimation = Tween<Offset>(begin: const Offset(0.0, -1.5), end: Offset.zero)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack));
_controller.forward();
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
_controller.reverse().then((_) => widget.onDismiss());
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final themeType = themeManager.currentThemeType;
final theme = themeManager.currentColors;
Color inkColor = const Color(0xFF111122);
return SlideTransition(
position: _offsetAnimation,
child: Material(
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : theme.background,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: themeType == AppThemeType.doodle ? inkColor : theme.playerBlue,
width: 2
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5)
)
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.circle, color: Colors.greenAccent, size: 14),
const SizedBox(width: 10),
Text(
"${widget.name} è online!",
style: getSharedTextStyle(
themeType,
TextStyle(
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
fontWeight: FontWeight.bold,
fontSize: 15
)
),
),
],
),
),
),
);
}
}
// ===========================================================================
// FILE: lib/ui/multiplayer/lobby_screen.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/ui/multiplayer/lobby_screen.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 '../../logic/game_controller.dart';
import '../../models/game_board.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
import '../../services/multiplayer_service.dart';
import '../../services/storage_service.dart';
import '../game/game_screen.dart';
import '../../widgets/painters.dart';
import '../../widgets/cyber_border.dart'; // <--- ECCO L'IMPORT MANCANTE!
import 'lobby_widgets.dart';
class LobbyScreen extends StatefulWidget {
final String? initialRoomCode;
const LobbyScreen({super.key, this.initialRoomCode});
@override
State<LobbyScreen> createState() => _LobbyScreenState();
}
class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
final MultiplayerService _multiplayerService = MultiplayerService();
late TextEditingController _codeController;
bool _isLoading = false;
String? _myRoomCode;
String _playerName = '';
bool _isCreatingRoom = false;
int _selectedRadius = 4;
ArenaShape _selectedShape = ArenaShape.classic;
bool _isTimeMode = true;
bool _isPublicRoom = true;
bool _roomStarted = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_codeController = TextEditingController();
_playerName = StorageService.instance.playerName;
if (widget.initialRoomCode != null && widget.initialRoomCode!.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() { _codeController.text = widget.initialRoomCode!; });
});
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_cleanupGhostRoom();
_codeController.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) {
_cleanupGhostRoom();
}
}
void _cleanupGhostRoom() {
if (_myRoomCode != null && !_roomStarted) {
FirebaseFirestore.instance.collection('games').doc(_myRoomCode).delete();
_myRoomCode = null;
}
}
Future<void> _createRoom() async {
if (_isLoading) return;
setState(() => _isLoading = true);
try {
String code = await _multiplayerService.createGameRoom(
_selectedRadius, _playerName, _selectedShape.name, _isTimeMode, isPublic: _isPublicRoom
);
if (!mounted) return;
setState(() { _myRoomCode = code; _isLoading = false; _roomStarted = false; });
if (!_isPublicRoom) {
_multiplayerService.shareInviteLink(code);
}
_showWaitingDialog(code);
} catch (e) {
if (mounted) { setState(() => _isLoading = false); _showError("Errore durante la creazione della partita."); }
}
}
Future<void> _createRoomAndInvite(String targetUid, String targetName) async {
if (_isLoading) return;
setState(() => _isLoading = true);
try {
String code = await _multiplayerService.createGameRoom(
_selectedRadius, _playerName, _selectedShape.name, _isTimeMode, isPublic: _isPublicRoom
);
await _multiplayerService.sendInvite(targetUid, code, _playerName);
if (!mounted) return;
setState(() { _myRoomCode = code; _isLoading = false; _roomStarted = false; });
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Sfida inviata a $targetName!"), backgroundColor: Colors.green));
_showWaitingDialog(code);
} catch (e) {
if (mounted) { setState(() => _isLoading = false); _showError("Errore durante la creazione della partita."); }
}
}
Future<void> _joinRoomByCode(String code) async {
if (_isLoading) return;
FocusScope.of(context).unfocus();
code = code.trim().toUpperCase();
if (code.isEmpty || code.length != 5) { _showError("Inserisci un codice valido di 5 caratteri."); return; }
setState(() => _isLoading = true);
try {
Map<String, dynamic>? roomData = await _multiplayerService.joinGameRoom(code, _playerName);
if (!mounted) return;
setState(() => _isLoading = false);
if (roomData != null) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza trovata! Partita in avvio..."), backgroundColor: Colors.green));
int hostRadius = roomData['radius'] ?? 4;
String shapeStr = roomData['shape'] ?? 'classic';
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
bool hostTimeMode = roomData['timeMode'] ?? true;
context.read<GameController>().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
} else {
_showError("Stanza non trovata, piena o partita già iniziata.");
}
} catch (e) {
if (mounted) { setState(() => _isLoading = false); _showError("Errore di connessione: $e"); }
}
}
void _showError(String message) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message, style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red)); }
void _showFavoritesDialogForCreation() {
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);
_createRoomAndInvite(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))))
],
);
}
);
}
void _showWaitingDialog(String code) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
final theme = context.watch<ThemeManager>().currentColors;
final themeType = context.read<ThemeManager>().currentThemeType;
Widget dialogContent = Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: theme.playerRed), const SizedBox(height: 25),
Text("CODICE STANZA", style: getLobbyTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.6), letterSpacing: 2))),
Text(code, style: getLobbyTextStyle(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!" : "Condividi link", textAlign: TextAlign.center, style: getLobbyTextStyle(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." : "Condividi il codice. La partita inizierà appena si unirà.", textAlign: TextAlign.center, style: getLobbyTextStyle(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: (context, snapshot) {
if (snapshot.hasData && snapshot.data!.exists) {
var data = snapshot.data!.data() as Map<String, dynamic>;
if (data['status'] == 'playing') {
_roomStarted = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pop(context);
context.read<GameController>().startNewGame(_selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: _selectedShape, timeMode: _isTimeMode);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
});
}
}
return PopScope(
canPop: false,
onPopInvoked: (didPop) {
if (didPop) return;
_cleanupGhostRoom();
Navigator.pop(context);
},
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
dialogContent,
const SizedBox(height: 20),
TextButton(
onPressed: () {
_cleanupGhostRoom();
Navigator.pop(context);
},
child: Text("ANNULLA", style: getLobbyTextStyle(themeType, TextStyle(color: Colors.red, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 2.0, shadows: themeType == AppThemeType.doodle ? [] : [const Shadow(color: Colors.black, blurRadius: 2)]))),
),
],
),
),
);
},
);
}
);
}
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final themeType = themeManager.currentThemeType;
final theme = themeManager.currentColors;
String? bgImage;
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';
bool isChaosUnlocked = StorageService.instance.playerLevel >= 7;
// --- MODIFICA COLORE SFONDO HOST PANEL ---
Color panelBackgroundColor = Colors.transparent;
if (themeType == AppThemeType.cyberpunk) {
panelBackgroundColor = Colors.black.withOpacity(0.1);
} else if (themeType == AppThemeType.doodle) {
panelBackgroundColor = Colors.white.withOpacity(0.5);
} else if (themeType == AppThemeType.grimorio) {
panelBackgroundColor = Colors.white.withOpacity(0.2); // Sfumatura bianca leggera per Grimorio
} else if (themeType == AppThemeType.arcade) {
panelBackgroundColor = Colors.black.withOpacity(0.4); // <-- AGGIUNGI QUESTO PER L'ARCADE
}
Widget hostPanel = Transform.rotate(
angle: themeType == AppThemeType.doodle ? 0.01 : 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15),
decoration: BoxDecoration(
color: panelBackgroundColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(themeType == AppThemeType.doodle ? 5 : 20),
topRight: const Radius.circular(20),
bottomLeft: const Radius.circular(20),
bottomRight: Radius.circular(themeType == AppThemeType.doodle ? 5 : 20),
),
border: themeType == AppThemeType.cyberpunk ? null : Border.all(color: themeType == AppThemeType.doodle ? theme.text.withOpacity(0.5) : Colors.white.withOpacity(0.15), width: themeType == AppThemeType.doodle ? 2 : 1.5),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(child: Text("IMPOSTAZIONI STANZA", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.6), letterSpacing: 2.0)))),
const SizedBox(height: 10),
Text("FORMA ARENA", style: getLobbyTextStyle(themeType, TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), letterSpacing: 1.5))),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: _selectedShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.classic))),
const SizedBox(width: 4),
Expanded(child: NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: _selectedShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.cross))),
const SizedBox(width: 4),
Expanded(child: NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: _selectedShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.donut))),
const SizedBox(width: 4),
Expanded(child: NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: _selectedShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.hourglass))),
const SizedBox(width: 4),
Expanded(child: NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: _selectedShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setState(() => _selectedShape = ArenaShape.chaos))),
],
),
const SizedBox(height: 12),
Divider(color: themeType == AppThemeType.doodle ? theme.text.withOpacity(0.5) : Colors.white.withOpacity(0.05), thickness: themeType == AppThemeType.doodle ? 2.5 : 1.5),
const SizedBox(height: 12),
Text("GRANDEZZA", style: getLobbyTextStyle(themeType, TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), letterSpacing: 1.5))),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
NeonSizeButton(label: 'S', isSelected: _selectedRadius == 3, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedRadius = 3)),
NeonSizeButton(label: 'M', isSelected: _selectedRadius == 4, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedRadius = 4)),
NeonSizeButton(label: 'L', isSelected: _selectedRadius == 5, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedRadius = 5)),
NeonSizeButton(label: 'MAX', isSelected: _selectedRadius == 6, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedRadius = 6)),
],
),
const SizedBox(height: 12),
Divider(color: themeType == AppThemeType.doodle ? theme.text.withOpacity(0.5) : Colors.white.withOpacity(0.05), thickness: themeType == AppThemeType.doodle ? 2.5 : 1.5),
const SizedBox(height: 12),
Text("TEMPO E OPZIONI", style: getLobbyTextStyle(themeType, TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), letterSpacing: 1.5))),
const SizedBox(height: 8),
Row(
children: [
Expanded(child: NeonTimeSwitch(isTimeMode: _isTimeMode, theme: theme, themeType: themeType, onTap: () => setState(() => _isTimeMode = !_isTimeMode))),
],
),
const SizedBox(height: 10),
Row(
children: [
Expanded(child: NeonPrivacySwitch(isPublic: _isPublicRoom, theme: theme, themeType: themeType, onTap: () => setState(() => _isPublicRoom = !_isPublicRoom))),
const SizedBox(width: 8),
Expanded(child: NeonInviteFavoriteButton(theme: theme, themeType: themeType, onTap: _showFavoritesDialogForCreation)),
],
)
],
),
),
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
hostPanel = AnimatedCyberBorder(child: hostPanel);
}
Widget uiContent = SafeArea(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.only(left: 20.0, right: 20.0, top: 10.0, bottom: MediaQuery.of(context).padding.bottom + 60.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back_ios_new, color: theme.text),
onPressed: () => Navigator.pop(context),
),
Expanded(
child: Text("MULTIPLAYER", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2))),
),
const SizedBox(width: 48),
],
),
const SizedBox(height: 20),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: _isCreatingRoom
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
hostPanel,
const SizedBox(height: 15),
Row(
children: [
Expanded(
child: NeonActionButton(label: "AVVIA", color: theme.playerRed, onTap: _createRoom, theme: theme, themeType: themeType),
),
const SizedBox(width: 10),
Expanded(
child: NeonActionButton(label: "ANNULLA", color: Colors.grey.shade600, onTap: () => setState(() => _isCreatingRoom = false), theme: theme, themeType: themeType),
),
],
),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NeonActionButton(label: "CREA PARTITA", color: theme.playerRed, onTap: () { FocusScope.of(context).unfocus(); setState(() => _isCreatingRoom = true); }, theme: theme, themeType: themeType),
const SizedBox(height: 20),
Row(
children: [
Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)),
Padding(padding: const EdgeInsets.symmetric(horizontal: 10), child: Text("OPPURE", style: getLobbyTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), fontWeight: FontWeight.bold, letterSpacing: 2.0, fontSize: 13)))),
Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)),
],
),
const SizedBox(height: 20),
Transform.rotate(
angle: themeType == AppThemeType.doodle ? 0.02 : 0,
child: Container(
decoration: themeType == AppThemeType.doodle ? BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), bottomRight: Radius.circular(20), topRight: Radius.circular(5), bottomLeft: Radius.circular(5)),
border: Border.all(color: theme.text, width: 2.5),
boxShadow: [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(5, 5), blurRadius: 0)],
) : BoxDecoration(
boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.15), blurRadius: 15, spreadRadius: 1)]
),
child: TextField(
controller: _codeController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 5,
style: getLobbyTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 12, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: theme.playerBlue.withOpacity(0.5), blurRadius: 8)])),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 12),
hintText: "CODICE", hintStyle: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.3), letterSpacing: 10, fontSize: 20)), counterText: "",
filled: themeType != AppThemeType.doodle,
fillColor: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.85) : theme.text.withOpacity(0.05),
enabledBorder: themeType == AppThemeType.doodle ? InputBorder.none : OutlineInputBorder(borderSide: BorderSide(color: theme.gridLine.withOpacity(0.5), width: 2.0), borderRadius: BorderRadius.circular(15)),
focusedBorder: themeType == AppThemeType.doodle ? InputBorder.none : OutlineInputBorder(borderSide: BorderSide(color: theme.playerBlue, width: 3.0), borderRadius: BorderRadius.circular(15)),
),
),
),
),
const SizedBox(height: 15),
NeonActionButton(label: "UNISCITI", color: theme.playerBlue, onTap: () => _joinRoomByCode(_codeController.text), theme: theme, themeType: themeType),
],
),
),
const SizedBox(height: 25),
Row(
children: [
Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)),
Padding(padding: const EdgeInsets.symmetric(horizontal: 10), child: Text("LOBBY PUBBLICA", style: getLobbyTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), fontWeight: FontWeight.bold, letterSpacing: 2.0, fontSize: 13)))),
Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)),
],
),
const SizedBox(height: 15),
StreamBuilder<QuerySnapshot>(
stream: _multiplayerService.getPublicRooms(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Padding(padding: const EdgeInsets.all(20), child: Center(child: CircularProgressIndicator(color: theme.playerBlue)));
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Center(child: Text("Nessuna stanza pubblica al momento.\nCreane una tu!", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), height: 1.5, fontSize: 16)))),
);
}
DateTime now = DateTime.now();
String? myUid = FirebaseAuth.instance.currentUser?.uid;
var docs = snapshot.data!.docs.where((doc) {
var data = doc.data() as Map<String, dynamic>;
if (data['isPublic'] != true) return false;
if (data['hostUid'] != null && data['hostUid'] == myUid) return false;
Timestamp? createdAt = data['createdAt'] as Timestamp?;
if (createdAt != null) {
int ageInMinutes = now.difference(createdAt.toDate()).inMinutes;
if (ageInMinutes > 15) {
FirebaseFirestore.instance.collection('games').doc(doc.id).delete();
return false;
}
}
return true;
}).toList();
if (docs.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Center(child: Text("Nessuna stanza pubblica al momento.\nCreane una tu!", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), height: 1.5, fontSize: 16)))),
);
}
docs.sort((a, b) {
Timestamp? tA = (a.data() as Map<String, dynamic>)['createdAt'] as Timestamp?;
Timestamp? tB = (b.data() as Map<String, dynamic>)['createdAt'] as Timestamp?;
if (tA == null || tB == null) return 0;
return tB.compareTo(tA);
});
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: docs.length,
itemBuilder: (context, index) {
var doc = docs[index];
var data = doc.data() as Map<String, dynamic>;
String host = data['hostName'] ?? 'Sconosciuto';
int r = data['radius'] ?? 4;
String shapeStr = data['shape'] ?? 'classic';
bool time = data['timeMode'] ?? true;
String prettyShape = "Rombo";
if (shapeStr == 'cross') prettyShape = "Croce";
else if (shapeStr == 'donut') prettyShape = "Buco";
else if (shapeStr == 'hourglass') prettyShape = "Clessidra";
else if (shapeStr == 'chaos') prettyShape = "Caos";
return Transform.rotate(
angle: themeType == AppThemeType.doodle ? (index % 2 == 0 ? 0.01 : -0.01) : 0,
child: Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: themeType == AppThemeType.doodle ? theme.text : theme.playerBlue.withOpacity(0.3), width: themeType == AppThemeType.doodle ? 2 : 1),
boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: theme.text.withOpacity(0.6), offset: const Offset(3, 4))] : [],
),
child: Row(
children: [
CircleAvatar(radius: 25, backgroundColor: theme.playerRed.withOpacity(0.2), child: Icon(Icons.person, color: theme.playerRed, size: 28)),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Stanza di $host", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 18))),
const SizedBox(height: 6),
Text("Raggio: $r • $prettyShape • ${time ? 'A Tempo' : 'Relax'}", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontSize: 12))),
],
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
backgroundColor: theme.playerBlue, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: themeType == AppThemeType.doodle ? 0 : 2,
side: themeType == AppThemeType.doodle ? BorderSide(color: theme.text, width: 2) : BorderSide.none,
),
onPressed: () => _joinRoomByCode(doc.id),
child: Text("ENTRA", style: getLobbyTextStyle(themeType, const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.0))),
)
],
),
),
);
}
);
}
),
],
),
),
);
return Scaffold(
backgroundColor: bgImage != null ? Colors.transparent : theme.background,
extendBodyBehindAppBar: true,
body: Stack(
children: [
Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background),
if (themeType == AppThemeType.doodle)
Positioned.fill(
child: CustomPaint(
painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)),
),
),
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 (themeType == AppThemeType.music)
Positioned.fill(
child: IgnorePointer(
child: CustomPaint(
painter: AudioCablesPainter(),
),
),
),
Positioned.fill(child: uiContent),
],
),
);
}
}
// ===========================================================================
// FILE: lib/ui/multiplayer/lobby_widgets.dart
// ===========================================================================
// ===========================================================================
// 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))]))),
),
),
),
),
);
}
}
// ===========================================================================
// 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:provider/provider.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
import '../../services/storage_service.dart';
import '../../widgets/painters.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
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(
backgroundColor: theme.background,
extendBodyBehindAppBar: true,
appBar: AppBar(
toolbarHeight: 80 * vScale,
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,
elevation: 0,
iconTheme: IconThemeData(color: Colors.white, size: 28 * vScale),
),
body: Stack(
children: [
Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background),
Positioned.fill(
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: const AssetImage('assets/images/sfondo_temi.jpg'),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.6), BlendMode.darken),
),
),
),
),
ListView(
padding: EdgeInsets.only(top: 120 * vScale, left: 20 * vScale, right: 20 * vScale, bottom: 40 * vScale),
physics: const BouncingScrollPhysics(),
children: [
_ThemeCard(
title: "Quaderno",
subtitle: "Sfondo a quadretti, tratto a penna",
type: AppThemeType.doodle,
previewColors: AppColors.doodle,
requiredLevel: 1,
currentLevel: playerLevel,
vScale: vScale,
),
SizedBox(height: 25 * vScale),
_ThemeCard(
title: "Cyberpunk",
subtitle: "Nero profondo, luci al neon",
type: AppThemeType.cyberpunk,
previewColors: AppColors.cyberpunk,
requiredLevel: 3,
currentLevel: playerLevel,
vScale: vScale,
),
SizedBox(height: 25 * vScale),
_ThemeCard(
title: "8-Bit Arcade",
subtitle: "Sale giochi, fosfori verdi e pixel",
type: AppThemeType.arcade,
previewColors: AppColors.arcade,
requiredLevel: 7,
currentLevel: playerLevel,
vScale: vScale,
),
SizedBox(height: 25 * vScale),
_ThemeCard(
title: "Grimorio",
subtitle: "Incantesimi antichi, rune magiche",
type: AppThemeType.grimorio,
previewColors: AppColors.grimorio,
requiredLevel: 10,
currentLevel: playerLevel,
vScale: vScale,
),
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),
],
),
],
),
);
}
}
class _ThemeCard extends StatelessWidget {
final String title;
final String subtitle;
final AppThemeType type;
final ThemeColors previewColors;
final int requiredLevel;
final int currentLevel;
final double vScale;
const _ThemeCard({
required this.title,
required this.subtitle,
required this.type,
required this.previewColors,
required this.requiredLevel,
required this.currentLevel,
required this.vScale,
});
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
bool isSelected = themeManager.currentThemeType == type;
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(
onTap: () {
if (isLocked) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Gioca per raggiungere il Liv. $requiredLevel e sbloccare questo tema!", style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white)),
backgroundColor: Colors.redAccent,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
duration: const Duration(seconds: 2),
)
);
return;
}
themeManager.setTheme(type);
Navigator.pop(context);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: 140 * vScale,
padding: EdgeInsets.symmetric(horizontal: 20 * vScale, vertical: 15 * vScale),
decoration: BoxDecoration(
color: isLocked ? Colors.black87 : previewColors.background,
borderRadius: BorderRadius.circular(20),
border: border,
boxShadow: shadows,
image: bgImage != null ? DecorationImage(
image: AssetImage(bgImage!),
fit: BoxFit.cover,
colorFilter: type == AppThemeType.doodle
? ColorFilter.mode(Colors.white.withOpacity(isLocked ? 0.9 : 0.4), BlendMode.lighten)
: ColorFilter.mode(Colors.black.withOpacity(isLocked ? 0.85 : 0.5), BlendMode.darken),
) : null,
),
child: Stack(
alignment: Alignment.center,
children: [
Opacity(
opacity: isLocked ? 0.3 : 1.0,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
// --- CORNICE EFFETTO VETRO (GLASSMORPHISM) ---
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 6.0, sigmaY: 6.0),
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: 28 * vScale, height: 28 * vScale,
decoration: BoxDecoration(
color: previewColors.playerRed,
shape: type == AppThemeType.arcade ? BoxShape.rectangle : BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
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)
Container(
padding: EdgeInsets.symmetric(horizontal: 16 * vScale, vertical: 10 * vScale),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.95),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: previewColors.playerRed.withOpacity(0.8), width: 2),
boxShadow: [BoxShadow(color: previewColors.playerRed.withOpacity(0.5), blurRadius: 20)],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.lock_rounded, color: Colors.white, size: 20 * vScale),
SizedBox(width: 8 * vScale),
Text(
"LIV. $requiredLevel",
style: getSharedTextStyle(type, TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 16 * vScale, letterSpacing: 2))
),
],
),
),
],
),
),
);
}
}
// ===========================================================================
// FILE: lib/widgets/custom_button.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/widgets/custom_settings_button.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/widgets/custom_settings_button.dart
// ===========================================================================
import 'package:flutter/material.dart';
import '../core/app_colors.dart';
import 'painters.dart'; // Importiamo i painter per i doodle e il font
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.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;
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: 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(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 16, 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),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
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(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))),
],
),
],
),
),
);
}
}
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: getSharedTextStyle(themeType, 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: 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))]))),
),
),
),
),
);
}
}
// ===========================================================================
// FILE: lib/widgets/cyber_border.dart
// ===========================================================================
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;
}
// ===========================================================================
// FILE: lib/widgets/game_over_dialog.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/widgets/game_over_dialog.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../logic/game_controller.dart';
import '../core/theme_manager.dart';
import '../core/app_colors.dart';
import '../services/storage_service.dart';
class GameOverDialog extends StatelessWidget {
const GameOverDialog({super.key});
@override
Widget build(BuildContext context) {
final game = context.read<GameController>();
final themeManager = context.read<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
int red = game.board.scoreRed;
int blue = game.board.scoreBlue;
bool playerBeatCPU = game.isVsCPU && red > blue;
String myName = StorageService.instance.playerName.toUpperCase();
if (myName.isEmpty) myName = "TU";
// --- LOGICA NOMI ---
String nameRed = myName;
String nameBlue = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU";
if (game.isOnline) {
nameRed = game.onlineHostName.toUpperCase();
nameBlue = game.onlineGuestName.toUpperCase();
} else if (game.isVsCPU) {
nameRed = myName;
nameBlue = "CPU";
}
// --- DETERMINA IL VINCITORE ---
String winnerText = "";
Color winnerColor = theme.text;
if (red > blue) {
winnerText = "VINCE $nameRed!";
winnerColor = theme.playerRed;
} else if (blue > red) {
winnerText = "VINCE $nameBlue!";
winnerColor = theme.playerBlue;
} else {
winnerText = "PAREGGIO!";
winnerColor = theme.text;
}
return AlertDialog(
backgroundColor: theme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2),
),
title: Text("FINE PARTITA", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22)),
content: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(winnerText, textAlign: TextAlign.center, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor)),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(15),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("$nameRed: $red", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed)),
Text(" - ", style: TextStyle(fontSize: 18, color: theme.text)),
Text("$nameBlue: $blue", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue)),
],
),
),
if (game.isVsCPU) ...[
const SizedBox(height: 15),
Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7))),
],
// --- SEZIONE LEVEL UP E ROADMAP DINAMICA ---
if (game.hasLeveledUp && game.unlockedRewards.isNotEmpty) ...[
const SizedBox(height: 30),
const Divider(),
const SizedBox(height: 15),
Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20),
decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.2),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.amber, width: 2)
),
child: Text("🎉 LIVELLO ${game.newlyReachedLevel}! 🎉", style: const TextStyle(color: Colors.amber, fontWeight: FontWeight.w900, fontSize: 18)),
),
const SizedBox(height: 15),
...game.unlockedRewards.map((reward) => Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: (reward['color'] as Color).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: (reward['color'] as Color).withOpacity(0.5), width: 1.5),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: (reward['color'] as Color).withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(reward['icon'], color: reward['color'], size: 28),
),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(reward['title'], style: TextStyle(color: reward['color'], fontWeight: FontWeight.w900, fontSize: 16)),
const SizedBox(height: 4),
Text(reward['desc'], style: TextStyle(color: theme.text.withOpacity(0.9), fontSize: 12, height: 1.3)),
],
)
)
]
)
)),
]
// ---------------------------------------------
],
),
),
actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10),
actionsAlignment: MainAxisAlignment.center,
actions: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (playerBeatCPU)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: winnerColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 5,
),
onPressed: () {
Navigator.pop(context);
game.increaseLevelAndRestart();
},
child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
)
else if (game.isOnline)
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: () {
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
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: () {
Navigator.pop(context);
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),
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 (game.isOnline) {
game.disconnectOnlineGame();
}
Navigator.pop(context);
Navigator.pop(context);
},
child: Text("TORNA AL MENU", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)),
),
],
)
],
);
}
}
// ===========================================================================
// FILE: lib/widgets/home_buttons.dart
// ===========================================================================
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),
],
),
),
);
}
}
// ===========================================================================
// FILE: lib/widgets/music_theme_widgets.dart
// ===========================================================================
// ===========================================================================
// 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)))
),
],
),
);
}
}
// ===========================================================================
// FILE: lib/widgets/painters.dart
// ===========================================================================
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;
}