=== FLUTTER PROJECT BACKUP === === PROJECT STRUCTURE (LIB, ASSETS & PUBLIC) === 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 public/404.html public/index.html public/report.html === pubspec.yaml === name: tetraq description: A new Flutter project. publish_to: 'none' version: 1.1.8+2 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 --- LSApplicationCategoryType public.app-category.puzzle-games CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication --- Entitlements --- com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.network.client com.apple.security.network.server keychain-access-groups com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.network.client keychain-access-groups --- 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 --- CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Tetraq CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName tetraq CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName com.sanza.tetraq CFBundleURLSchemes tetraq CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight NSAppTransportSecurity NSAllowsArbitraryLoads --- 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 --- --- 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 = "../.." } === WEB / FIREBASE (public/) === // =========================================================================== // FILE: public/404.html // =========================================================================== Page Not Found

404

Page Not Found

The specified file was not found on this website. Please check the URL for mistakes and try again.

Why am I seeing this?

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

// =========================================================================== // FILE: public/index.html // =========================================================================== Gioca a TetraQ!

Apertura in corso... 🚀

// =========================================================================== // FILE: public/report.html // =========================================================================== Report Giocatori - TetraQ

Area Riservata

📊 Report Statistiche TetraQ

Totale Giocatori

0

Apple iOS

0

Google Android

0

Desktop / Mac

0

Ultimo Accesso Giocatore Statistiche Connessione Dispositivo
Caricamento dati dal database in corso...
=== 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 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 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, you’ll need to edit this /// file. /// /// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. /// Then, in the Project Navigator, open the Info.plist file under the Runner /// project’s Runner folder. /// /// Next, select the Information Property List item, select Add Item from the /// Editor menu, then select Localizations from the pop-up menu. /// /// Select and expand the newly-created Localizations item then, for each /// locale your application supports, add a new item and select the locale /// you wish to add from the pop-up menu in the Value field. This list should /// be consistent with the languages listed in the AppLocalizations.supportedLocales /// property. abstract class AppLocalizations { AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; static AppLocalizations? of(BuildContext context) { return Localizations.of(context, AppLocalizations); } static const LocalizationsDelegate 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> localizationsDelegates = >[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ Locale('de'), Locale('en'), Locale('es'), Locale('fr'), Locale('it'), Locale('pt'), Locale('ru'), Locale('zh'), ]; /// No description provided for @appTitle. /// /// In it, this message translates to: /// **'TetraQ'** String get appTitle; /// No description provided for @welcomeTitle. /// /// In it, this message translates to: /// **'BENVENUTO IN TETRAQ!'** String get welcomeTitle; /// No description provided for @nameHint. /// /// In it, this message translates to: /// **'NOME'** String get nameHint; /// No description provided for @saveAndPlay. /// /// In it, this message translates to: /// **'SALVA E GIOCA'** String get saveAndPlay; /// No description provided for @onlineTitle. /// /// In it, this message translates to: /// **'ONLINE'** String get onlineTitle; /// No description provided for @onlineSub. /// /// In it, this message translates to: /// **'Sfida il mondo'** String get onlineSub; /// No description provided for @cpuTitle. /// /// In it, this message translates to: /// **'VS CPU'** String get cpuTitle; /// No description provided for @cpuSub. /// /// In it, this message translates to: /// **'Allenati con l\'IA'** String get cpuSub; /// No description provided for @localTitle. /// /// In it, this message translates to: /// **'LOCALE'** String get localTitle; /// No description provided for @localSub. /// /// In it, this message translates to: /// **'Stesso schermo'** String get localSub; /// No description provided for @leaderboardTitle. /// /// In it, this message translates to: /// **'CLASSIFICA'** String get leaderboardTitle; /// No description provided for @questsTitle. /// /// In it, this message translates to: /// **'SFIDE'** String get questsTitle; /// No description provided for @themesTitle. /// /// In it, this message translates to: /// **'TEMI'** String get themesTitle; /// No description provided for @tutorialTitle. /// /// In it, this message translates to: /// **'TUTORIAL'** String get tutorialTitle; /// No description provided for @startGame. /// /// In it, this message translates to: /// **'AVVIA PARTITA'** String get startGame; /// No description provided for @createMatch. /// /// In it, this message translates to: /// **'CREA PARTITA'** String get createMatch; /// No description provided for @joinMatch. /// /// In it, this message translates to: /// **'UNISCITI'** String get joinMatch; /// No description provided for @gameOver. /// /// In it, this message translates to: /// **'FINE PARTITA'** String get gameOver; /// No description provided for @mainMenu. /// /// In it, this message translates to: /// **'TORNA AL MENU'** String get mainMenu; /// No description provided for @exit. /// /// In it, this message translates to: /// **'ESCI'** String get exit; /// No description provided for @roomSettings. /// /// In it, this message translates to: /// **'IMPOSTAZIONI STANZA'** String get roomSettings; /// No description provided for @arenaShape. /// /// In it, this message translates to: /// **'FORMA ARENA'** String get arenaShape; /// No description provided for @arenaSize. /// /// In it, this message translates to: /// **'GRANDEZZA'** String get arenaSize; /// No description provided for @timeAndOptions. /// /// In it, this message translates to: /// **'TEMPO E OPZIONI'** String get timeAndOptions; /// No description provided for @timeLabel. /// /// In it, this message translates to: /// **'TEMPO'** String get timeLabel; /// No description provided for @btnStart. /// /// In it, this message translates to: /// **'AVVIA'** String get btnStart; /// No description provided for @btnCancel. /// /// In it, this message translates to: /// **'ANNULLA'** String get btnCancel; /// No description provided for @wordOr. /// /// In it, this message translates to: /// **'OPPURE'** String get wordOr; /// No description provided for @codeHint. /// /// In it, this message translates to: /// **'CODICE'** String get codeHint; /// No description provided for @publicLobbyTitle. /// /// In it, this message translates to: /// **'LOBBY PUBBLICA'** String get publicLobbyTitle; /// No description provided for @emptyLobbyMsg. /// /// In it, this message translates to: /// **'Nessuna stanza pubblica al momento.\nCreane una tu!'** String get emptyLobbyMsg; /// No description provided for @roomOf. /// /// In it, this message translates to: /// **'Stanza di'** String get roomOf; /// No description provided for @btnEnter. /// /// In it, this message translates to: /// **'ENTRA'** String get btnEnter; } class _AppLocalizationsDelegate extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override Future load(Locale locale) { return SynchronousFuture(lookupAppLocalizations(locale)); } @override bool isSupported(Locale locale) => [ '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'; @override String get roomSettings => 'IMPOSTAZIONI STANZA'; @override String get arenaShape => 'FORMA ARENA'; @override String get arenaSize => 'GRANDEZZA'; @override String get timeAndOptions => 'TEMPO E OPZIONI'; @override String get timeLabel => 'TEMPO'; @override String get btnStart => 'AVVIA'; @override String get btnCancel => 'ANNULLA'; @override String get wordOr => 'OPPURE'; @override String get codeHint => 'CODICE'; @override String get publicLobbyTitle => 'LOBBY PUBBLICA'; @override String get emptyLobbyMsg => 'Nessuna stanza pubblica al momento.\nCreane una tu!'; @override String get roomOf => 'Stanza di'; @override String get btnEnter => 'ENTRA'; } // =========================================================================== // 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'; @override String get roomSettings => 'ROOM SETTINGS'; @override String get arenaShape => 'ARENA SHAPE'; @override String get arenaSize => 'SIZE'; @override String get timeAndOptions => 'TIME & OPTIONS'; @override String get timeLabel => 'TIME'; @override String get btnStart => 'START'; @override String get btnCancel => 'CANCEL'; @override String get wordOr => 'OR'; @override String get codeHint => 'CODE'; @override String get publicLobbyTitle => 'PUBLIC LOBBY'; @override String get emptyLobbyMsg => 'No public rooms right now.\nCreate one!'; @override String get roomOf => 'Room of'; @override String get btnEnter => 'ENTER'; } // =========================================================================== // 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'; @override String get roomSettings => 'IMPOSTAZIONI STANZA'; @override String get arenaShape => 'FORMA ARENA'; @override String get arenaSize => 'GRANDEZZA'; @override String get timeAndOptions => 'TEMPO E OPZIONI'; @override String get timeLabel => 'TEMPO'; @override String get btnStart => 'AVVIA'; @override String get btnCancel => 'ANNULLA'; @override String get wordOr => 'OPPURE'; @override String get codeHint => 'CODICE'; @override String get publicLobbyTitle => 'LOBBY PUBBLICA'; @override String get emptyLobbyMsg => 'Nessuna stanza pubblica al momento.\nCreane una tu!'; @override String get roomOf => 'Stanza di'; @override String get btnEnter => 'ENTRA'; } // =========================================================================== // 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'; @override String get roomSettings => 'IMPOSTAZIONI STANZA'; @override String get arenaShape => 'FORMA ARENA'; @override String get arenaSize => 'GRANDEZZA'; @override String get timeAndOptions => 'TEMPO E OPZIONI'; @override String get timeLabel => 'TEMPO'; @override String get btnStart => 'AVVIA'; @override String get btnCancel => 'ANNULLA'; @override String get wordOr => 'OPPURE'; @override String get codeHint => 'CODICE'; @override String get publicLobbyTitle => 'LOBBY PUBBLICA'; @override String get emptyLobbyMsg => 'Nessuna stanza pubblica al momento.\nCreane una tu!'; @override String get roomOf => 'Stanza di'; @override String get btnEnter => 'ENTRA'; } // =========================================================================== // 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'; @override String get roomSettings => 'IMPOSTAZIONI STANZA'; @override String get arenaShape => 'FORMA ARENA'; @override String get arenaSize => 'GRANDEZZA'; @override String get timeAndOptions => 'TEMPO E OPZIONI'; @override String get timeLabel => 'TEMPO'; @override String get btnStart => 'AVVIA'; @override String get btnCancel => 'ANNULLA'; @override String get wordOr => 'OPPURE'; @override String get codeHint => 'CODICE'; @override String get publicLobbyTitle => 'LOBBY PUBBLICA'; @override String get emptyLobbyMsg => 'Nessuna stanza pubblica al momento.\nCreane una tu!'; @override String get roomOf => 'Stanza di'; @override String get btnEnter => 'ENTRA'; } // =========================================================================== // 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'; @override String get roomSettings => 'IMPOSTAZIONI STANZA'; @override String get arenaShape => 'FORMA ARENA'; @override String get arenaSize => 'GRANDEZZA'; @override String get timeAndOptions => 'TEMPO E OPZIONI'; @override String get timeLabel => 'TEMPO'; @override String get btnStart => 'AVVIA'; @override String get btnCancel => 'ANNULLA'; @override String get wordOr => 'OPPURE'; @override String get codeHint => 'CODICE'; @override String get publicLobbyTitle => 'LOBBY PUBBLICA'; @override String get emptyLobbyMsg => 'Nessuna stanza pubblica al momento.\nCreane una tu!'; @override String get roomOf => 'Stanza di'; @override String get btnEnter => 'ENTRA'; } // =========================================================================== // 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 => 'ВЫХОД'; @override String get roomSettings => 'IMPOSTAZIONI STANZA'; @override String get arenaShape => 'FORMA ARENA'; @override String get arenaSize => 'GRANDEZZA'; @override String get timeAndOptions => 'TEMPO E OPZIONI'; @override String get timeLabel => 'TEMPO'; @override String get btnStart => 'AVVIA'; @override String get btnCancel => 'ANNULLA'; @override String get wordOr => 'OPPURE'; @override String get codeHint => 'CODICE'; @override String get publicLobbyTitle => 'LOBBY PUBBLICA'; @override String get emptyLobbyMsg => 'Nessuna stanza pubblica al momento.\nCreane una tu!'; @override String get roomOf => 'Stanza di'; @override String get btnEnter => 'ENTRA'; } // =========================================================================== // 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 => '退出'; @override String get roomSettings => 'IMPOSTAZIONI STANZA'; @override String get arenaShape => 'FORMA ARENA'; @override String get arenaSize => 'GRANDEZZA'; @override String get timeAndOptions => 'TEMPO E OPZIONI'; @override String get timeLabel => 'TEMPO'; @override String get btnStart => 'AVVIA'; @override String get btnCancel => 'ANNULLA'; @override String get wordOr => 'OPPURE'; @override String get codeHint => 'CODICE'; @override String get publicLobbyTitle => 'LOBBY PUBBLICA'; @override String get emptyLobbyMsg => 'Nessuna stanza pubblica al momento.\nCreane una tu!'; @override String get roomOf => 'Stanza di'; @override String get btnEnter => 'ENTRA'; } // =========================================================================== // 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; final bool isIceTrap; _ClosureResult(this.closesSomething, this.netValue, this.causesSwap, this.isIceTrap); } class AIEngine { static Line getBestMove(GameBoard board, int level) { List availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList(); final random = Random(); if (availableLines.isEmpty) return board.lines.first; // Più il livello è alto, più l'IA è "intelligente" 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; // --- NUOVA LOGICA: GESTIONE INVERSIONE (TACTICAL FEEDING) --- // Se c'è un numero dispari di caselle Scambio aperte, il gioco è "invertito". // I punti accumulati andranno in regalo all'avversario! int swapCount = board.boxes.where((b) => b.type == BoxType.swap && !b.isClosed()).length; bool isInverted = swapCount % 2 != 0; List goodClosingMoves = []; List badClosingMoves = []; List iceTraps = []; for (var line in availableLines) { var result = _checkClosure(board, line, isInverted); if (result.isIceTrap) { iceTraps.add(line); continue; } if (result.closesSomething) { if (result.causesSwap) { if (myScore < oppScore) { goodClosingMoves.add(line); // Se perdiamo, lo scambio è la mossa vincente! } else { badClosingMoves.add(line); // Se vinciamo, NON tocchiamo lo scambio! } } 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 safeMoves = []; for (var line in availableLines) { if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && !iceTraps.contains(line) && _isSafeMove(board, line, myScore, oppScore, isInverted)) { 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 riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l) && !iceTraps.contains(l)).toList(); if (riskyButNotTerrible.isNotEmpty) { return riskyButNotTerrible[random.nextInt(riskyButNotTerrible.length)]; } } // Ultima spiaggia List nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l) && !iceTraps.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, bool isInverted) { int netValue = 0; bool closesSomething = false; bool causesSwap = false; bool isIceTrap = 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) { if (box.type == BoxType.ice && !line.isIceCracked) { isIceTrap = true; } else { closesSomething = true; if (box.type == BoxType.swap) { causesSwap = true; } else { int boxValue = 0; if (box.hiddenJokerOwner == board.currentPlayer) { boxValue = 2; } else { if (box.type == BoxType.gold) boxValue = 2; else if (box.type == BoxType.bomb) boxValue = -1; else if (box.type == BoxType.ice) boxValue = 0; else if (box.type == BoxType.multiplier) boxValue = 1; else boxValue = 1; } // LA MAGIA: Se il gioco è invertito, fare punti positivi viene calcolato come MALUS per l'IA! netValue += isInverted ? -boxValue : boxValue; } } } } } return _ClosureResult(closesSomething, netValue, causesSwap, isIceTrap); } static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore, bool isInverted) { 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) { int valueForOpponent = 0; if (box.type == BoxType.ice) { valueForOpponent = -5; } else if (box.type == BoxType.swap) { if (myScore < oppScore) { continue; // Sicuro lasciarlo: se lo prende perde i punti. } else { return false; // Pericoloso: se lo prende ci ruba il vantaggio! } } else if (box.hiddenJokerOwner == board.currentPlayer) { valueForOpponent = -1; } else { if (box.type == BoxType.gold) valueForOpponent = 2; else if (box.type == BoxType.bomb) valueForOpponent = -1; else if (box.type == BoxType.multiplier) valueForOpponent = 1; else valueForOpponent = 1; } // LA MAGIA 2: Se il tabellone è invertito, regalare un punto all'avversario è un'ottima esca! if (isInverted && box.type != BoxType.swap && box.type != BoxType.ice) { valueForOpponent = -valueForOpponent; } if (valueForOpponent < 0) { continue; // Mossa considerata sicura (trappola perfetta) } 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? _onlineSubscription; bool opponentLeft = false; bool _hasSavedResult = false; Timer? _blitzTimer; int timeLeft = 10; int maxTime = 10; String timeModeSetting = 'fixed'; // 'fixed', 'relax', 'dynamic' bool get isTimeMode => timeModeSetting != 'relax'; int consecutiveRematches = 0; // Contatore per la modalità Dinamica 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>> 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> 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 hardShapes = [ArenaShape.classic, ArenaShape.cross, ArenaShape.donut, ArenaShape.hourglass, ArenaShape.chaos]; ArenaShape chosenShape = hardShapes[rand.nextInt(hardShapes.length)]; int chosenRadius = (chosenShape == ArenaShape.chaos) ? (rand.nextInt(2) + 4) : (rand.nextInt(2) + 5); return CpuMatchSetup(chosenRadius, chosenShape); } void startNewGame(int radius, {bool vsCPU = false, bool isOnline = false, String? roomCode, bool isHost = false, ArenaShape shape = ArenaShape.classic, String timeMode = 'fixed', bool isRematch = false}) { _onlineSubscription?.cancel(); _onlineSubscription = 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; if (!isRematch) consecutiveRematches = 0; this.timeModeSetting = timeMode; // --- LOGICA TIMER --- if (this.isVsCPU) { int pLevel = StorageService.instance.playerLevel; int calculatedTime = 15 - ((pLevel - 1) * 12 / 14).round(); maxTime = calculatedTime.clamp(3, 15); } else { if (timeModeSetting == 'dynamic') { maxTime = max(2, 10 - (consecutiveRematches * 2)); } else if (timeModeSetting == 'relax') { maxTime = 0; } else { maxTime = 10; } } timeLeft = maxTime; int finalRadius = radius; ArenaShape finalShape = shape; if (this.isVsCPU) { CpuMatchSetup setup = _getSetupForCpuLevel(cpuLevel); finalRadius = setup.radius; finalShape = setup.shape; } onlineShape = finalShape; int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel; 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 newClosed, {List 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 || !isTimeMode) return; timeLeft = maxTime; _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 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; onlineHostName = data['hostName'] ?? "ROSSO"; onlineGuestName = (data['guestName'] != null && data['guestName'] != '') ? data['guestName'] : "BLU"; // 1. GESTIONE ABBANDONO if (data['status'] == 'abandoned' && !board.isGameOver && !opponentLeft) { opponentLeft = true; notifyListeners(); return; } // 2. GESTIONE REAZIONI 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); } // 3. LOGICA RIVINCITA MIGLIORATA bool p1Rematch = data['p1_rematch'] ?? false; bool p2Rematch = data['p2_rematch'] ?? false; opponentWantsRematch = isHost ? p2Rematch : p1Rematch; // SOLO L'HOST si occupa di chiamare resetMatch sul server if (isHost && p1Rematch && p2Rematch && 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)]; // Questo cambierà lo status in 'playing' e svuoterà l'array moves. MultiplayerService().resetMatch(roomCode!, newRadius, newShape.name, newSeed); return; // L'host aspetterà il prossimo trigger dal server con il nuovo seed. } 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); String hostTimeMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax'); // TUTTI (Host e Guest) ripartono SOLO quando vedono il reset effettivo (nuovo seed e status 'playing') if (rematchRequested && data['status'] == 'playing' && hostSeed != null && hostSeed != currentSeed) { currentSeed = hostSeed; consecutiveRematches++; startNewGame(hostRadius, isOnline: true, roomCode: roomCode, isHost: isHost, shape: hostShape, timeMode: hostTimeMode, isRematch: true); return; } // 4. GESTIONE FASE INIZIALE (JOLLY) if (isSetupPhase) { if (!isHost && data['p1_joker'] != null && !oppJokerPlaced) { int jx = data['p1_joker']['x']; int jy = data['p1_joker']['y']; board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.red; oppJokerPlaced = true; _checkSetupComplete(); } if (isHost && data['p2_joker'] != null && !oppJokerPlaced) { int jx = data['p2_joker']['x']; int jy = data['p2_joker']['y']; board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.blue; oppJokerPlaced = true; _checkSetupComplete(); } } // 5. AGGIORNAMENTO LIVELLO / SEED (se non in rivincita) int hostLevel = data['matchLevel'] ?? 1; onlineShape = hostShape; timeModeSetting = hostTimeMode; 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; } // 6. GESTIONE MOSSE List moves = data['moves'] ?? []; int firebaseMovesCount = moves.length; int localMovesCount = board.lines.where((l) => l.owner != Player.none).length; // Resilienza: se il locale ha mosse e il server no (e non stiamo aspettando una rivincita), pulisci. if (firebaseMovesCount == 0 && localMovesCount > 0 && !rematchRequested) { int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel; board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape); board.currentPlayer = Player.red; notifyListeners(); return; } // Applica mosse remote 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 closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList(); board.playMove(lineToPlay, forcedPlayer: playerFromFirebase); newMovesApplied = true; List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); List 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 closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList(); if (board.playMove(line)) { List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); List 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 moveData = { 'id': DateTime.now().millisecondsSinceEpoch, '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 closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList(); Line bestMove = AIEngine.getBestMove(board, cpuLevel); board.playMove(bestMove); List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); List 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> _getUnlocks(int oldLevel, int newLevel) { List> unlocks = []; for(int i = oldLevel + 1; i <= newLevel; i++) { if (rewardsRoadmap.containsKey(i)) { unlocks.addAll(rewardsRoadmap[i]!); } } return unlocks; } Future _saveMatchResult() async { 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; await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true); if (isWin) await 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) { await StorageService.instance.addWin(); await StorageService.instance.updateQuestProgress(1, 1); } else if (cpuScore > myScore) { await StorageService.instance.addLoss(); } await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false); } else { calculatedXP = 2; await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false); } if (board.shape != ArenaShape.classic) { await StorageService.instance.updateQuestProgress(2, 1); } lastMatchXP = calculatedXP; await 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: timeModeSetting); } } // =========================================================================== // 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'; import 'package:upgrader/upgrader.dart'; import 'package:in_app_update/in_app_update.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:tetraq/l10n/app_localizations.dart'; void main() async { 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 { // --- BUG FIX: Creiamo l'account anonimo SOLO se non c'è una sessione attiva --- // In questo modo, una volta fatto il login, non verrai più buttato fuori al riavvio! if (FirebaseAuth.instance.currentUser == null) { await FirebaseAuth.instance.signInAnonymously(); } } catch (e) { debugPrint("Errore Auth: $e"); } await StorageService.instance.init(); await 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, ), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: UpdateWrapper(child: HomeScreen()), ); } } // =========================================================================== // WIDGET WRAPPER PER LA GESTIONE DEGLI AGGIORNAMENTI IBRIDI (iOS/Android) // =========================================================================== class UpdateWrapper extends StatefulWidget { final Widget child; const UpdateWrapper({super.key, required this.child}); @override State createState() => _UpdateWrapperState(); } class _UpdateWrapperState extends State { @override void initState() { super.initState(); if (!kIsWeb && Platform.isAndroid) { _checkForAndroidUpdate(); } } Future _checkForAndroidUpdate() async { try { final info = await InAppUpdate.checkForUpdate(); if (info.updateAvailability == UpdateAvailability.updateAvailable) { if (info.flexibleUpdateAllowed) { await InAppUpdate.startFlexibleUpdate(); await InAppUpdate.completeFlexibleUpdate(); } else if (info.immediateUpdateAllowed) { await InAppUpdate.performImmediateUpdate(); } } } catch (e) { debugPrint("Errore in_app_update Android: $e"); } } @override Widget build(BuildContext context) { if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) { return UpgradeAlert( dialogStyle: (Platform.isIOS || Platform.isMacOS) ? UpgradeDialogStyle.cupertino : UpgradeDialogStyle.material, showIgnore: false, showLater: true, upgrader: Upgrader(), child: widget.child, ); } return widget.child; } } // =========================================================================== // 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 } 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; 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; return 1; } } class GameBoard { final int radius; final int level; final int? seed; final ArenaShape shape; late int columns; late int rows; List dots = []; List lines = []; List boxes = []; Player currentPlayer = Player.red; int scoreRed = 0; int scoreBlue = 0; bool isGameOver = false; Line? lastMove; 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; else if (level >= 15 && chance > 0.78 && chance <= 0.83) box.type = BoxType.multiplier; } boxes.add(box); } } // ========================================================= // NUOVO BLOCCO: ELIMINAZIONE SCAMBI PARI // ========================================================= int swapCount = boxes.where((b) => b.type == BoxType.swap).length; if (swapCount > 0 && swapCount % 2 == 0) { Box lastSwap = boxes.lastWhere((b) => b.type == BoxType.swap); lastSwap.type = BoxType.normal; } // ========================================================= 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; lastMove = actualLine; if (forcedPlayer == null) currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; else currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; return true; } actualLine.isIceCracked = false; actualLine.owner = playerMakingMove; 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) { if (playerMakingMove == Player.red && redHasMultiplier) { points *= 2; redHasMultiplier = false; } else if (playerMakingMove == Player.blue && blueHasMultiplier) { points *= 2; blueHasMultiplier = false; } } if (playerMakingMove == Player.red) { scoreRed += points; } else { scoreBlue += points; } if (box.type == BoxType.swap && box.hiddenJokerOwner == null) { triggeredSwap = true; } } if (box.type == BoxType.invisible && !box.isRevealed) { if (box.top.owner != Player.none && box.bottom.owner != Player.none && box.left.owner != Player.none && box.right.owner != Player.none) { box.isRevealed = true; } } } if (triggeredSwap) { 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; // Abbiamo rimosso _sfxPlayer perché ora ogni suono crea un player usa e getta final AudioPlayer _bgmPlayer = AudioPlayer(); AppThemeType _currentTheme = AppThemeType.doodle; Future 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(); } else { playBgm(_currentTheme); } notifyListeners(); } Future 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 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 { final player = AudioPlayer(); // Player dedicato await player.play(AssetSource('audio/sfx/$file'), volume: 1.0); player.onPlayerComplete.listen((_) => player.dispose()); } catch (e) { debugPrint("Errore SFX Linea: $e"); } } } void playBoxSfx(AppThemeType theme) async { 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 { final player = AudioPlayer(); // Player dedicato await player.play(AssetSource('audio/sfx/$file'), volume: 1.0); player.onPlayerComplete.listen((_) => player.dispose()); } catch (e) { debugPrint("Errore SFX Box: $e"); } } } void playBonusSfx() async { if (isMuted) return; try { final player = AudioPlayer(); // Player dedicato await player.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0); player.onPlayerComplete.listen((_) => player.dispose()); } catch(e) {} } void playBombSfx() async { if (isMuted) return; try { final player = AudioPlayer(); // Player dedicato await player.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0); player.onPlayerComplete.listen((_) => player.dispose()); } 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'); // --- MODIFICA QUI: bool isTimeMode è diventato String timeMode --- Future createGameRoom(int boardRadius, String hostName, String shapeName, String timeMode, {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': timeMode, // Salva la stringa ('fixed', 'relax' o 'dynamic') 'isPublic': isPublic, 'p1_reaction': null, 'p2_reaction': null, 'p1_rematch': false, 'p2_rematch': false, }); return roomCode; } Future?> 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; } return null; } Stream 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 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 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 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 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 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 listenForInvites(String myUid) { return _invitesCollection.where('targetUid', isEqualTo: myUid).snapshots(); } Future 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 'dart:async'; // <--- AGGIUNTO PER IL TIMER DELL'HEARTBEAT 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; Timer? _heartbeatTimer; // <--- IL NOSTRO BATTITO CARDIACO Future init() async { _prefs = await SharedPreferences.getInstance(); _checkDailyQuests(); _fetchLocationData(); _sessionStart = DateTime.now().millisecondsSinceEpoch; } // --- NUOVI METODI PER GESTIRE LA PRESENZA --- void startHeartbeat() { _heartbeatTimer?.cancel(); // Esegue il sync leggero ogni 60 secondi _heartbeatTimer = Timer.periodic(const Duration(seconds: 120), (_) { syncLeaderboard(isHeartbeat: true); }); } void stopHeartbeat() { _heartbeatTimer?.cancel(); } // ---------------------------------------------- Future _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 saveTheme(String themeStr) async => await _prefs.setString('theme', themeStr); int get savedRadius => _prefs.getInt('radius') ?? 2; Future saveRadius(int radius) async => await _prefs.setInt('radius', radius); bool get isMuted => _prefs.getBool('isMuted') ?? false; Future saveMuted(bool muted) async => await _prefs.setBool('isMuted', muted); int get totalXP => _prefs.getInt('totalXP') ?? 0; // --- SICUREZZA XP: Inviamo solo INCREMENTI al server --- Future addXP(int xp) async { await _prefs.setInt('totalXP', totalXP + xp); final user = FirebaseAuth.instance.currentUser; if (user != null) { await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({ 'xp': FieldValue.increment(xp), 'level': playerLevel, }, SetOptions(merge: true)); } } int get playerLevel => (totalXP / 100).floor() + 1; int get wins => _prefs.getInt('wins') ?? 0; Future addWin() async { await _prefs.setInt('wins', wins + 1); final user = FirebaseAuth.instance.currentUser; if (user != null) { await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({ 'wins': FieldValue.increment(1), }, SetOptions(merge: true)); } } int get losses => _prefs.getInt('losses') ?? 0; Future addLoss() async { await _prefs.setInt('losses', losses + 1); final user = FirebaseAuth.instance.currentUser; if (user != null) { await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({ 'losses': FieldValue.increment(1), }, SetOptions(merge: true)); } } int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1; Future saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level); String get playerName => _prefs.getString('playerName') ?? ''; Future savePlayerName(String name) async { await _prefs.setString('playerName', name); syncLeaderboard(); } // ====================================================================== // LOGICA SYNC AGGIORNATA: GESTIONE HEARTBEAT LEGGERO // ====================================================================== Future syncLeaderboard({bool isHeartbeat = false}) async { try { final user = FirebaseAuth.instance.currentUser; if (user == null) return; String name = playerName; if (name.isEmpty) name = "GIOCATORE"; String targetUid = user.uid; // 1. Calcolo del Playtime effettivo (aggiornato ad ogni sync) int sessionDurationSec = (DateTime.now().millisecondsSinceEpoch - _sessionStart) ~/ 1000; int savedPlaytime = _prefs.getInt('total_playtime') ?? 0; int totalPlaytime = savedPlaytime + sessionDurationSec; await _prefs.setInt('total_playtime', totalPlaytime); _sessionStart = DateTime.now().millisecondsSinceEpoch; // Resetta il timer di sessione // 2. Creazione del payload di base (dati leggeri che cambiano spesso) Map dataToSave = { 'name': name, 'level': playerLevel, 'lastActive': FieldValue.serverTimestamp(), 'playtime': totalPlaytime, }; // 3. Se NON è un heartbeat, raccogliamo anche i dati "pesanti" (Device info, ecc.) if (!isHeartbeat) { String appVer = "N/D"; String devModel = "N/D"; String osName = kIsWeb ? "Web" : Platform.operatingSystem; try { PackageInfo packageInfo = await PackageInfo.fromPlatform(); appVer = "${packageInfo.version}+${packageInfo.buildNumber}"; DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (!kIsWeb) { if (Platform.isAndroid) { AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; devModel = "${androidInfo.brand} ${androidInfo.model}".toUpperCase(); osName = "Android"; } else if (Platform.isIOS) { IosDeviceInfo iosInfo = await deviceInfo.iosInfo; devModel = iosInfo.utsname.machine; // Es. "iPhone13,2" osName = "iOS"; } else if (Platform.isMacOS) { MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo; devModel = macInfo.model; // Es. "MacBookPro17,1" osName = "macOS"; } } } catch (e) { debugPrint("Errore device info: $e"); } dataToSave['appVersion'] = appVer; dataToSave['deviceModel'] = devModel; dataToSave['platform'] = osName; dataToSave['ip'] = lastIp; dataToSave['city'] = lastCity; if (user.metadata.creationTime != null) { dataToSave['accountCreated'] = Timestamp.fromDate(user.metadata.creationTime!); } } await FirebaseFirestore.instance.collection('leaderboard').doc(targetUid).set(dataToSave, SetOptions(merge: true)); } catch (e) { debugPrint("Errore durante la sincronizzazione della classifica: $e"); } } Future isUserAdmin() async { try { final user = FirebaseAuth.instance.currentUser; if (user == null) return false; final doc = await FirebaseFirestore.instance.collection('admins').doc(user.uid).get(); return doc.exists; } catch (e) { debugPrint("Errore verifica admin: $e"); return false; } } List> get favorites { List favs = _prefs.getStringList('favorites') ?? []; return favs.map((e) => Map.from(jsonDecode(e))).toList(); } Future 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 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> get matchHistory { List history = _prefs.getStringList('matchHistory') ?? []; return history.map((e) => jsonDecode(e) as Map).toList(); } Future saveMatchToHistory({required String myName, required String opponent, required int myScore, required int oppScore, required bool isOnline}) async { List history = _prefs.getStringList('matchHistory') ?? []; Map 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().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( 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 name = data['name'] ?? 'Fantasma'; int level = data['level'] ?? 1; int xp = data['xp'] ?? 0; int wins = data['wins'] ?? 0; String platform = data['platform'] ?? 'Sconosciuta'; String ip = data['ip'] ?? 'N/D'; String city = data['city'] ?? 'N/D'; String appVersion = data['appVersion'] ?? 'N/D'; String deviceModel = data['deviceModel'] ?? 'N/D'; int playtimeSec = data['playtime'] ?? 0; int hours = playtimeSec ~/ 3600; int minutes = (playtimeSec % 3600) ~/ 60; String playtimeStr = "${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}"; // Recupero della data di creazione dell'account DateTime? created; if (data['accountCreated'] != null) created = (data['accountCreated'] as Timestamp).toDate(); DateTime? lastActive; if (data['lastActive'] != null) lastActive = (data['lastActive'] as Timestamp).toDate(); String createdStr = created != null ? DateFormat('dd MMM yyyy - HH:mm').format(created) : 'N/D'; String lastActiveStr = lastActive != null ? DateFormat('dd MMM yyyy - HH:mm').format(lastActive) : 'N/D'; IconData platformIcon = Icons.device_unknown; if (platform == 'iOS' || platform == 'macOS') platformIcon = Icons.apple; if (platform == 'Android') platformIcon = Icons.android; if (platform == 'Windows') platformIcon = Icons.window; return Card( color: theme.text.withOpacity(0.05), elevation: 0, margin: const EdgeInsets.only(bottom: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), side: BorderSide(color: theme.gridLine.withOpacity(0.3)) ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(name, style: TextStyle(color: theme.playerBlue, fontSize: 22, fontWeight: FontWeight.w900)), GestureDetector( onTap: () { showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: theme.background, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide(color: theme.playerBlue, width: 2), ), title: Text("Info Connessione", style: TextStyle(color: theme.text, fontWeight: FontWeight.bold)), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("🌐 IP: $ip", style: TextStyle(color: theme.text, fontSize: 16)), const SizedBox(height: 10), Text("📍 Città: $city", style: TextStyle(color: theme.text, fontSize: 16)), const SizedBox(height: 10), Text("📱 OS: $platform", style: TextStyle(color: theme.text, fontSize: 16)), const SizedBox(height: 10), Text("💻 Hardware: $deviceModel", style: TextStyle(color: theme.text, fontSize: 16)), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: Text("CHIUDI", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold)), ) ], ), ); }, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: theme.text.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon(platformIcon, color: theme.text.withOpacity(0.8), size: 24), ), ), ], ), const SizedBox(height: 8), Row( children: [ Text("Liv. $level", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 14)), const SizedBox(width: 10), Text("$xp XP", style: TextStyle(color: theme.text.withOpacity(0.7), fontSize: 12)), const SizedBox(width: 10), Text("Vittorie: $wins", style: TextStyle(color: Colors.amber.shade700, fontWeight: FontWeight.bold, fontSize: 12)), const Spacer(), Icon(Icons.timer, color: theme.text.withOpacity(0.6), size: 16), const SizedBox(width: 4), Text(playtimeStr, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 14)), ], ), const Padding( padding: EdgeInsets.symmetric(vertical: 8.0), child: Divider(), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FittedBox(fit: BoxFit.scaleDown, child: Text("Registrato il:", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10))), FittedBox(fit: BoxFit.scaleDown, child: Text(createdStr, style: TextStyle(color: theme.text, fontSize: 12, fontWeight: FontWeight.bold))), ], ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ FittedBox(fit: BoxFit.scaleDown, child: Text("Versione App:", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10))), FittedBox(fit: BoxFit.scaleDown, child: Text("v. $appVersion", style: TextStyle(color: theme.playerBlue, fontSize: 12, fontWeight: FontWeight.bold))), ], ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ FittedBox(fit: BoxFit.scaleDown, child: Text("Ultimo Accesso:", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10))), FittedBox(fit: BoxFit.scaleDown, child: Text(lastActiveStr, style: TextStyle(color: Colors.green, fontSize: 12, fontWeight: FontWeight.bold))), ], ), ), ], ) ], ), ), ); }, ); } ), ); } } // =========================================================================== // 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 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 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 createState() => _GameScreenState(); } class _GameScreenState extends State 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(); } // --- NUOVO DIALOG: CONFERMA USCITA (ANTI-FUGA) --- void _showExitConfirmationDialog(BuildContext context, GameController gameController, ThemeColors theme, AppThemeType themeType) { showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: theme.background, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide(color: theme.playerRed, width: 2), ), title: Row( children: [ Icon(Icons.warning_amber_rounded, color: theme.playerRed), const SizedBox(width: 10), Expanded( child: Text( "ABBANDONARE?", style: _getTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 18)) ) ), ], ), content: Text( "Se esci ora, la partita verrà registrata automaticamente come una SCONFITTA.\n\nSei sicuro di voler fuggire?", style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 15, height: 1.4)), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: Text("ANNULLA", style: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontWeight: FontWeight.bold))), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), onPressed: () { // 1. Assegna la sconfitta! StorageService.instance.addLoss(); // 2. Disconnette e pulisce gameController.disconnectOnlineGame(); // 3. Chiude il dialog Navigator.pop(ctx); // 4. Torna al menu Navigator.pop(context); }, child: Text("SÌ, ESCI", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))), ), ], ), ); } void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) { _gameOverDialogShown = true; showDialog( barrierDismissible: false, context: context, builder: (dialogContext) => Consumer( 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.timeModeSetting); }, 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(); final themeType = themeManager.currentThemeType; final theme = themeManager.currentColors; final gameController = context.watch(); 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 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), // --- NUOVO ON PRESSED ANTI-FUGA --- onPressed: () { if (!gameController.isGameOver && !gameController.isSetupPhase) { _showExitConfirmationDialog(context, gameController, theme, themeType); } else { gameController.disconnectOnlineGame(); Navigator.pop(context); } }, // ------------------------ label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))), ), ), ], ), ) ], ), if (gameController.myReaction != null) Positioned(top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!)), if (gameController.opponentReaction != null) Positioned(top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!)), ], ), ); // --- NUOVA LOGICA: Impedisce la chiusura accidentale o la fuga (Tasto Indietro) --- bool shouldConfirmExit = !gameController.isGameOver && !gameController.isSetupPhase; return PopScope( canPop: !shouldConfirmExit, onPopInvoked: (didPop) { if (didPop) { gameController.disconnectOnlineGame(); return; } if (shouldConfirmExit) { _showExitConfirmationDialog(context, gameController, theme, themeType); } }, 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 { 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 createState() => _WinnerVFXOverlayState(); } class _WinnerVFXOverlayState extends State 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 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 _anim; @override void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 500))..repeat(reverse: true); _anim = Tween(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 createState() => _BlitzBackgroundEffectState(); } class _BlitzBackgroundEffectState extends State 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 createState() => _SpecialEventBackgroundEffectState(); } class _SpecialEventBackgroundEffectState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _scaleAnimation; late Animation _opacityAnimation; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward(); _scaleAnimation = Tween(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); _opacityAnimation = Tween(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 createState() => _ScoreBoardState(); } class _ScoreBoardState extends State { @override Widget build(BuildContext context) { final controller = context.watch(); final themeManager = context.watch(); 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(); final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType; final loc = AppLocalizations.of(context)!; return FutureBuilder( future: SharedPreferences.getInstance(), builder: (context, snapshot) { if (!snapshot.hasData) return const SizedBox(); final prefs = snapshot.data!; return Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: Container( padding: const EdgeInsets.all(25.0), decoration: BoxDecoration( gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]), borderRadius: BorderRadius.circular(25), border: Border.all(color: theme.playerBlue.withOpacity(0.5), width: 2), boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.2), blurRadius: 20, spreadRadius: 5)] ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.assignment_turned_in, size: 50, color: theme.playerBlue), const SizedBox(height: 10), Text(loc.questsTitle, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), const SizedBox(height: 25), ...List.generate(3, (index) { int i = index + 1; int type = prefs.getInt('q${i}_type') ?? 0; int prog = prefs.getInt('q${i}_prog') ?? 0; int target = prefs.getInt('q${i}_target') ?? 1; String title = ""; IconData icon = Icons.star; if (type == 0) { title = "Vinci partite Online"; icon = Icons.public; } else if (type == 1) { title = "Vinci contro la CPU"; icon = Icons.smart_toy; } else { title = "Gioca in Arene Speciali"; icon = Icons.extension; } bool completed = prog >= target; double percent = (prog / target).clamp(0.0, 1.0); return Container( margin: const EdgeInsets.only(bottom: 15), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: completed ? Colors.green.withOpacity(0.1) : theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15), border: Border.all(color: completed ? Colors.green : theme.gridLine.withOpacity(0.3)), ), child: Row( children: [ Icon(icon, color: completed ? Colors.green : theme.text.withOpacity(0.6), size: 30), const SizedBox(width: 15), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: completed ? Colors.green : theme.text))), const SizedBox(height: 6), ClipRRect( borderRadius: BorderRadius.circular(10), child: LinearProgressIndicator(value: percent, backgroundColor: theme.gridLine.withOpacity(0.2), color: completed ? Colors.green : theme.playerBlue, minHeight: 8), ) ], ), ), const SizedBox(width: 10), Text("$prog / $target", style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.6)))), ], ), ); }), const SizedBox(height: 15), SizedBox( width: double.infinity, height: 50, child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () => Navigator.pop(context), child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), ), ) ], ), ), ); } ); } } // =========================================================================== // 2. DIALOGO CLASSIFICA (LEADERBOARD) CON CALLBACK SFIDA // =========================================================================== class LeaderboardDialog extends StatelessWidget { final Function(String uid, String name)? onChallenge; const LeaderboardDialog({super.key, this.onChallenge}); @override Widget build(BuildContext context) { final themeManager = context.watch(); 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( 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 name = (data['name'] ?? '').toString().toUpperCase(); // Nascondiamo PIPPO dalla classifica return name != 'PIPPO'; }).toList(); if (filteredDocs.isEmpty) { return Center(child: Text("Ancora nessun campione...", style: TextStyle(color: theme.text.withOpacity(0.5)))); } return ListView.builder( physics: const BouncingScrollPhysics(), itemCount: filteredDocs.length, itemBuilder: (context, index) { var doc = filteredDocs[index]; var data = doc.data() as Map; String? myUid = FirebaseAuth.instance.currentUser?.uid; bool isMe = doc.id == myUid; String playerName = data['name'] ?? 'Unknown'; bool isOnline = false; if (data['lastActive'] != null) { Timestamp lastActive = data['lastActive']; int diffInSeconds = DateTime.now().difference(lastActive.toDate()).inSeconds; if (diffInSeconds.abs() < 180) isOnline = true; } return StatefulBuilder( builder: (context, setStateItem) { bool isFav = StorageService.instance.isFavorite(doc.id); return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: isMe ? theme.playerBlue.withOpacity(0.2) : theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(10), border: isMe ? Border.all(color: theme.playerBlue, width: 1.5) : null ), child: Row( children: [ Text("#${index + 1}", style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.w900, color: index == 0 ? Colors.amber : (index == 1 ? Colors.grey.shade400 : (index == 2 ? Colors.brown.shade300 : theme.text.withOpacity(0.5)))))), const SizedBox(width: 15), Expanded( child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( playerName, style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: isMe ? FontWeight.w900 : FontWeight.bold, color: theme.text)), overflow: TextOverflow.ellipsis, ) ), if (isFav && !isMe && isOnline) ...[ const SizedBox(width: 8), PulsingChallengeButton( themeType: themeType, onTap: () { Navigator.pop(context); if (onChallenge != null) { onChallenge!(doc.id, playerName); } }, ), ] ], ), ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text("Lv. ${data['level'] ?? 1}", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 12)), Text("${data['xp'] ?? 0} XP", style: TextStyle(color: theme.text.withOpacity(0.6), fontSize: 10)), ], ), if (!isMe) ...[ const SizedBox(width: 8), GestureDetector( onTap: () async { await StorageService.instance.toggleFavorite(doc.id, playerName); setStateItem(() {}); }, child: Icon(isFav ? Icons.star : Icons.star_border, color: Colors.amber, size: 24), ) ] ], ), ); } ); } ); } ), ), const SizedBox(height: 15), SizedBox( width: double.infinity, height: 50, child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.amber.shade700, foregroundColor: Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () => Navigator.pop(context), child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), ), ) ], ), ); if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { content = AnimatedCyberBorder(child: content); } return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: content); } } // =========================================================================== // 3. DIALOGO TUTORIAL // =========================================================================== class TutorialDialog extends StatelessWidget { const TutorialDialog({super.key}); @override Widget build(BuildContext context) { final themeManager = context.watch(); 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 createState() => _PulsingChallengeButtonState(); } class _PulsingChallengeButtonState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 900))..repeat(reverse: true); _animation = Tween(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().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_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(); final themeType = themeManager.currentThemeType; Color inkColor = const Color(0xFF111122); final loc = AppLocalizations.of(dialogContext)!; return StatefulBuilder( builder: (context, setStateDialog) { Future handleAuth(bool isLogin) async { final name = nameController.text.trim(); final password = passController.text.trim(); setStateDialog(() { errorMessage = ""; isLoadingAuth = true; }); if (name.isEmpty || password.isEmpty) { setStateDialog(() { errorMessage = "Inserisci Nome e Password!"; isLoadingAuth = false; }); return; } if (password.length < 6) { setStateDialog(() { errorMessage = "La password deve avere almeno 6 caratteri!"; isLoadingAuth = false; }); return; } final fakeEmail = "${name.toLowerCase().replaceAll(' ', '')}@tetraq.game"; final currentUser = FirebaseAuth.instance.currentUser; final ghostUid = (currentUser != null && currentUser.isAnonymous) ? currentUser.uid : null; try { if (isLogin) { if (ghostUid != null) { await FirebaseFirestore.instance.collection('leaderboard').doc(ghostUid).delete().catchError((e) => null); } await FirebaseAuth.instance.signInWithEmailAndPassword(email: fakeEmail, password: password); final doc = await FirebaseFirestore.instance.collection('leaderboard').doc(FirebaseAuth.instance.currentUser!.uid).get(); if (doc.exists) { final data = doc.data() as Map; final prefs = await SharedPreferences.getInstance(); await prefs.setInt('totalXP', data['xp'] ?? 0); await prefs.setInt('wins', data['wins'] ?? 0); await prefs.setInt('losses', data['losses'] ?? 0); if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Bentornato $name! Dati sincronizzati."), backgroundColor: Colors.green)); } else { StorageService.instance.syncLeaderboard(); if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Bentornato $name! Profilo ripristinato."), backgroundColor: Colors.green)); } } else { if (currentUser != null && currentUser.isAnonymous) { final credential = EmailAuthProvider.credential(email: fakeEmail, password: password); await currentUser.linkWithCredential(credential); if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Profilo Cloud protetto con successo!"), backgroundColor: Colors.green)); } else { await FirebaseAuth.instance.createUserWithEmailAndPassword(email: fakeEmail, password: password); if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Account creato con successo!"), backgroundColor: Colors.green)); } } await StorageService.instance.savePlayerName(name); StorageService.instance.syncLeaderboard(); if (context.mounted) Navigator.of(dialogContext).pop(); onSuccess(); } on FirebaseAuthException catch (e) { String msg = "Errore di autenticazione."; if (e.code == 'email-already-in-use' || e.code == 'credential-already-in-use') { msg = "Nome già registrato!\nSe sei tu, clicca su ACCEDI."; } else if (e.code == 'user-not-found' || e.code == 'wrong-password' || e.code == 'invalid-credential') { msg = "Nome o Password errati!"; if (isLogin && ghostUid != null) StorageService.instance.syncLeaderboard(); } else if (e.code == 'requires-recent-login') { msg = "Errore di sessione. Riavvia l'app."; } setStateDialog(() { errorMessage = msg; isLoadingAuth = false; }); } catch (e) { setStateDialog(() { errorMessage = "Errore imprevisto: $e"; isLoadingAuth = false; }); if (isLogin && ghostUid != null) StorageService.instance.syncLeaderboard(); } } Widget dialogContent = themeType == AppThemeType.doodle ? CustomPaint( painter: DoodleBackgroundPainter(fillColor: Colors.yellow.shade100, strokeColor: inkColor, seed: 100), child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(loc.welcomeTitle, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900, fontSize: 24, letterSpacing: 2.0)), textAlign: TextAlign.center), const SizedBox(height: 10), Text('Scegli una Password per il Cloud.\nI tuoi XP e il tuo Livello saranno protetti e non li perderai mai!', style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 13, fontWeight: FontWeight.bold)), textAlign: TextAlign.center), const SizedBox(height: 15), TextField( controller: nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 4)), decoration: InputDecoration( hintText: loc.nameHint, hintStyle: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.3), letterSpacing: 4)), filled: false, counterText: "", enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: inkColor, width: 3)), focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.red.shade200, width: 5)) ), ), const SizedBox(height: 8), TextField( controller: passController, obscureText: obscurePassword, textAlign: TextAlign.center, maxLength: 20, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 8)), decoration: InputDecoration( hintText: "PASSWORD", hintStyle: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.3), letterSpacing: 4)), filled: false, counterText: "", enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: inkColor, width: 3)), focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.red.shade200, width: 5)), suffixIcon: IconButton( icon: Icon(obscurePassword ? Icons.visibility : Icons.visibility_off, color: inkColor.withOpacity(0.6)), onPressed: () { setStateDialog(() { obscurePassword = !obscurePassword; }); }, ), ), ), const SizedBox(height: 15), if (errorMessage.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 10), child: Text(errorMessage, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.bold)), textAlign: TextAlign.center)), Text("💡 Usa una password facile da ricordare!", style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.6), fontSize: 11, height: 1.3)), textAlign: TextAlign.center), const SizedBox(height: 15), isLoadingAuth ? CircularProgressIndicator(color: inkColor) : Row( children: [ Expanded(child: GestureDetector(onTap: () => handleAuth(true), child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.blue.shade200, strokeColor: inkColor, seed: 101), child: Container(height: 45, alignment: Alignment.center, child: Text("ACCEDI", style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 14, fontWeight: FontWeight.bold, letterSpacing: 1.5))))))), const SizedBox(width: 10), Expanded(child: GestureDetector(onTap: () => handleAuth(false), child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.green.shade200, strokeColor: inkColor, seed: 102), child: Container(height: 45, alignment: Alignment.center, child: Text("REGISTRATI", style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 14, fontWeight: FontWeight.bold, letterSpacing: 1.5))))))), ], ), ], ), ), ), ) : Container( decoration: BoxDecoration(color: themeManager.currentColors.background, borderRadius: BorderRadius.circular(25), border: Border.all(color: themeManager.currentColors.playerBlue.withOpacity(0.5), width: 2), boxShadow: [BoxShadow(color: themeManager.currentColors.playerBlue.withOpacity(0.3), blurRadius: 20, spreadRadius: 5)]), child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(loc.welcomeTitle, style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 1.5)), textAlign: TextAlign.center), const SizedBox(height: 10), Text('Scegli una Password per il Cloud.\nI tuoi XP e il tuo Livello saranno protetti e non li perderai mai!', style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.8), fontSize: 13, fontWeight: FontWeight.bold)), textAlign: TextAlign.center), const SizedBox(height: 15), TextField( controller: nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8, style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text, fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 4)), decoration: InputDecoration( hintText: loc.nameHint, hintStyle: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.3), letterSpacing: 4)), filled: true, fillColor: themeManager.currentColors.text.withOpacity(0.05), counterText: "", enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.gridLine.withOpacity(0.5), width: 2), borderRadius: BorderRadius.circular(15)), focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.playerBlue, width: 3), borderRadius: BorderRadius.circular(15)) ), ), const SizedBox(height: 10), TextField( controller: passController, obscureText: obscurePassword, textAlign: TextAlign.center, maxLength: 20, style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text, fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 8)), decoration: InputDecoration( hintText: "PASSWORD", hintStyle: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.3), letterSpacing: 4)), filled: true, fillColor: themeManager.currentColors.text.withOpacity(0.05), counterText: "", enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.gridLine.withOpacity(0.5), width: 2), borderRadius: BorderRadius.circular(15)), focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.playerBlue, width: 3), borderRadius: BorderRadius.circular(15)), suffixIcon: IconButton( icon: Icon(obscurePassword ? Icons.visibility : Icons.visibility_off, color: themeManager.currentColors.text.withOpacity(0.6)), onPressed: () { setStateDialog(() { obscurePassword = !obscurePassword; }); }, ), ), ), const SizedBox(height: 15), if (errorMessage.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 10), child: Text(errorMessage, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.redAccent, fontSize: 14, fontWeight: FontWeight.bold)), textAlign: TextAlign.center)), Text("💡 Usa una password facile da ricordare!", style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.6), fontSize: 11, height: 1.3)), textAlign: TextAlign.center), const SizedBox(height: 20), isLoadingAuth ? CircularProgressIndicator(color: themeManager.currentColors.playerBlue) : Row( children: [ Expanded(child: SizedBox(height: 45, child: ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: themeManager.currentColors.text.withOpacity(0.1), foregroundColor: themeManager.currentColors.text, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), side: BorderSide(color: themeManager.currentColors.playerBlue, width: 1.5)), onPressed: () => handleAuth(true), child: Text("ACCEDI", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, letterSpacing: 1.0)))))), const SizedBox(width: 10), Expanded(child: SizedBox(height: 45, child: ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: themeManager.currentColors.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () => handleAuth(false), child: Text("REGISTRATI", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, letterSpacing: 1.0)))))), ], ), ], ), ), ), ); if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) dialogContent = AnimatedCyberBorder(child: dialogContent); return PopScope( canPop: false, child: Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: dialogContent) ); }, ); }, ); } static Widget _buildTimeOption(String label, String sub, String value, String current, ThemeColors theme, AppThemeType type, VoidCallback onTap) { bool isSel = value == current; return Expanded( child: GestureDetector( onTap: onTap, child: Container( margin: const EdgeInsets.symmetric(horizontal: 4), height: 50, decoration: BoxDecoration( color: isSel ? Colors.orange.shade600 : (type == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05)), borderRadius: BorderRadius.circular(12), border: Border.all(color: isSel ? Colors.orange.shade800 : (type == AppThemeType.doodle ? const Color(0xFF111122) : Colors.white24), width: isSel ? 2 : 1.5), boxShadow: isSel && type != AppThemeType.doodle ? [BoxShadow(color: Colors.orange.withOpacity(0.5), blurRadius: 8)] : [], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(label, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white : (type == AppThemeType.doodle ? const Color(0xFF111122) : theme.text), fontWeight: FontWeight.w900, fontSize: 13))), if (sub.isNotEmpty) Text(sub, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white70 : (type == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.5)), fontWeight: FontWeight.bold, fontSize: 8))), ], ), ), ), ); } static void showChallengeSetupDialog(BuildContext context, String targetName, Function(int radius, ArenaShape shape, String timeMode) onStart) { int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed'; bool isChaosUnlocked = StorageService.instance.playerLevel >= 7; showDialog( context: context, barrierColor: Colors.black.withOpacity(0.8), builder: (ctx) { final themeManager = ctx.watch(); final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType; Color inkColor = const Color(0xFF111122); return StatefulBuilder( builder: (context, setStateDialog) { Widget dialogContent = themeType == AppThemeType.doodle ? Transform.rotate( angle: 0.015, child: CustomPaint( painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 200), child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets.all(25.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text("SFIDA $targetName", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 2))), const SizedBox(height: 10), Text("IMPOSTAZIONI STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 25), Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), Wrap( spacing: 12, runSpacing: 12, alignment: WrapAlignment.center, children: [ NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)), NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)), NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), ], ), const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)), NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)), NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)), NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)), ], ), const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), Text("TEMPO E OPZIONI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10), Row( children: [ _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), ], ), const SizedBox(height: 35), Row( children: [ Expanded( child: GestureDetector( onTap: () { Navigator.pop(ctx); onStart(localRadius, localShape, localTimeMode); }, child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: theme.playerRed, strokeColor: inkColor, seed: 300), child: Container(height: 55, alignment: Alignment.center, child: Text("AVVIA", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white))))), ), ), const SizedBox(width: 15), Expanded( child: GestureDetector( onTap: () => Navigator.pop(ctx), child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.grey.shade400, strokeColor: inkColor, seed: 301), child: Container(height: 55, alignment: Alignment.center, child: Text("ANNULLA", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white))))), ), ), ], ) ], ), ), ), ), ) : Container( decoration: BoxDecoration( gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]), borderRadius: BorderRadius.circular(25), border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5), boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))], ), child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text("SFIDA $targetName", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 2))), const SizedBox(height: 10), Text("IMPOSTAZIONI STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 20), Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), Wrap( spacing: 10, runSpacing: 10, alignment: WrapAlignment.center, children: [ NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)), NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)), NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), ], ), const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)), NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)), NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)), NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)), ], ), const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), Text("TEMPO E OPZIONI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), Row( children: [ _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), ], ), const SizedBox(height: 30), Row( children: [ Expanded( child: SizedBox( height: 55, child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () { Navigator.pop(ctx); onStart(localRadius, localShape, localTimeMode); }, child: const Text("AVVIA", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), ), ), ), const SizedBox(width: 15), Expanded( child: SizedBox( height: 55, child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.grey.shade800, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () => Navigator.pop(ctx), child: const Text("ANNULLA", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), ), ), ), ], ) ], ), ), ), ); if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { dialogContent = AnimatedCyberBorder(child: dialogContent); } return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), child: dialogContent); }, ); } ); } static void showMatchSetupDialog(BuildContext context, bool isVsCPU) { int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed'; bool isChaosUnlocked = StorageService.instance.playerLevel >= 7; final loc = AppLocalizations.of(context)!; showDialog( context: context, barrierColor: Colors.black.withOpacity(0.8), builder: (ctx) { final themeManager = ctx.watch(); final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType; Color inkColor = const Color(0xFF111122); return StatefulBuilder( builder: (context, setStateDialog) { Widget dialogContent = themeType == AppThemeType.doodle ? Transform.rotate( angle: 0.015, child: CustomPaint( painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 200), child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets.all(25.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row(children: [ SizedBox(width: 40, child: IconButton(padding: EdgeInsets.zero, alignment: Alignment.centerLeft, icon: Icon(Icons.arrow_back_ios_new, color: inkColor, size: 26), onPressed: () => Navigator.pop(ctx))), Expanded(child: Text(isVsCPU ? loc.cpuTitle : loc.localTitle, textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2)))), const SizedBox(width: 40) ]), const SizedBox(height: 25), if (isVsCPU) ...[ Icon(Icons.smart_toy, size: 50, color: inkColor.withOpacity(0.6)), const SizedBox(height: 10), Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: inkColor))), const SizedBox(height: 10), Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: inkColor.withOpacity(0.8), height: 1.4))), const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), ] else ...[ Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), Wrap( spacing: 12, runSpacing: 12, alignment: WrapAlignment.center, children: [ NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)), NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)), NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), ], ), const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)), NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)), NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)), NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)), ], ), const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10), Row( children: [ _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), ], ), const SizedBox(height: 35), ], Transform.rotate( angle: -0.02, child: GestureDetector( onTap: () { Navigator.pop(ctx); context.read().startNewGame(localRadius, vsCPU: isVsCPU, shape: localShape, timeMode: localTimeMode); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); }, child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.green.shade200, strokeColor: inkColor, seed: 300), child: Container(height: 65, width: double.infinity, alignment: Alignment.center, child: Text(loc.startGame, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: inkColor))))), ), ) ], ), ), ), ), ) : Container( decoration: BoxDecoration( gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]), borderRadius: BorderRadius.circular(25), border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5), boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))], ), child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row(children: [ SizedBox(width: 40, child: IconButton(padding: EdgeInsets.zero, alignment: Alignment.centerLeft, icon: Icon(Icons.arrow_back_ios_new, color: theme.text, size: 26), onPressed: () => Navigator.pop(ctx))), Expanded(child: Text(isVsCPU ? loc.cpuTitle : loc.localTitle, textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2)))), const SizedBox(width: 40) ]), const SizedBox(height: 20), if (isVsCPU) ...[ Icon(Icons.smart_toy, size: 50, color: theme.playerBlue), const SizedBox(height: 10), Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), const SizedBox(height: 10), Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: theme.text.withOpacity(0.7), height: 1.4))), const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), ] else ...[ Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), Wrap( spacing: 10, runSpacing: 10, alignment: WrapAlignment.center, children: [ NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)), NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)), NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), ], ), const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)), NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)), NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)), NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)), ], ), const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), Row( children: [ _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), ], ), const SizedBox(height: 30), ], SizedBox( width: double.infinity, height: 60, child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: isVsCPU ? Colors.purple.shade400 : theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), onPressed: () { Navigator.pop(ctx); context.read().startNewGame(localRadius, vsCPU: isVsCPU, shape: localShape, timeMode: localTimeMode); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); }, child: Text(loc.startGame, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2)), ), ) ], ), ), ), ); if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { dialogContent = AnimatedCyberBorder(child: dialogContent); } return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), child: dialogContent); }, ); } ); } static void showWaitingDialog({ required BuildContext context, required String code, required bool isPublicRoom, required int selectedRadius, required ArenaShape selectedShape, required String selectedTimeMode, required MultiplayerService multiplayerService, required VoidCallback onRoomStarted, required VoidCallback onCleanup, }) { showDialog( context: context, barrierDismissible: false, builder: (dialogContext) { final theme = dialogContext.watch().currentColors; final themeType = dialogContext.read().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( stream: multiplayerService.listenToRoom(code), builder: (ctx, snapshot) { if (snapshot.hasData && snapshot.data!.exists) { var data = snapshot.data!.data() as Map; if (data['status'] == 'playing') { onRoomStarted(); WidgetsBinding.instance.addPostFrameCallback((_) { Navigator.pop(ctx); context.read().startNewGame(selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: selectedShape, timeMode: selectedTimeMode); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); }); } } return PopScope( canPop: false, onPopInvoked: (didPop) { if (didPop) return; onCleanup(); Navigator.pop(ctx); }, child: Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, children: [ dialogContent, const SizedBox(height: 20), TextButton( onPressed: () { onCleanup(); Navigator.pop(ctx); }, child: Text("ANNULLA", style: getSharedTextStyle(themeType, TextStyle(color: Colors.red, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 2.0, shadows: themeType == AppThemeType.doodle ? [] : [const Shadow(color: Colors.black, blurRadius: 2)]))), ), ], ), ), ); }, ); } ); } static void showJoinPromptDialog(BuildContext context, String roomCode, Function(String) onConfirm) { showDialog( context: context, builder: (context) { final themeManager = context.watch(); 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(); 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 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.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 'package:package_info_plus/package_info_plus.dart'; import 'package:upgrader/upgrader.dart'; import 'package:in_app_update/in_app_update.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 createState() => _HomeScreenState(); } class _HomeScreenState extends State with WidgetsBindingObserver { int _debugTapCount = 0; late AppLinks _appLinks; StreamSubscription? _linkSubscription; StreamSubscription? _favoritesSubscription; StreamSubscription? _invitesSubscription; Map _lastOnlineNotifications = {}; final int _selectedRadius = 4; final ArenaShape _selectedShape = ArenaShape.classic; final bool _isPublicRoom = true; bool _isLoading = false; String? _myRoomCode; bool _roomStarted = false; String _appVersion = ''; bool _updateAvailable = false; final MultiplayerService _multiplayerService = MultiplayerService(); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); // --- AVVIA IL BATTITO CARDIACO --- StorageService.instance.startHeartbeat(); WidgetsBinding.instance.addPostFrameCallback((_) { if (StorageService.instance.playerName.isEmpty) { HomeModals.showNameDialog(context, () { StorageService.instance.syncLeaderboard(); _listenToInvites(); setState(() {}); }); } else { StorageService.instance.syncLeaderboard(); _listenToInvites(); } _checkThemeSafety(); }); _checkClipboardForInvite(); _initDeepLinks(); _listenToFavoritesOnline(); _loadAppVersion(); _checkStoreForUpdate(); } Future _loadAppVersion() async { try { PackageInfo packageInfo = await PackageInfo.fromPlatform(); if (mounted) { setState(() { _appVersion = "v. ${packageInfo.version}"; }); } } catch (e) { debugPrint("Errore lettura versione: $e"); } } Future _checkStoreForUpdate() async { if (kIsWeb) return; try { if (Platform.isAndroid) { final info = await InAppUpdate.checkForUpdate(); if (info.updateAvailability == UpdateAvailability.updateAvailable) { if (mounted) setState(() => _updateAvailable = true); } } else if (Platform.isIOS || Platform.isMacOS) { final upgrader = Upgrader(); await upgrader.initialize(); if (upgrader.isUpdateAvailable()) { if (mounted) setState(() => _updateAvailable = true); } } } catch (e) { debugPrint("Errore controllo aggiornamenti: $e"); } } void _triggerUpdate() async { if (kIsWeb) return; if (Platform.isAndroid) { try { final info = await InAppUpdate.checkForUpdate(); if (info.updateAvailability == UpdateAvailability.updateAvailable) { await InAppUpdate.performImmediateUpdate(); } } catch(e) { Upgrader().sendUserToAppStore(); } } else { Upgrader().sendUserToAppStore(); } } void _checkThemeSafety() { String themeStr = StorageService.instance.getTheme(); bool exists = AppThemeType.values.any((e) => e.toString() == themeStr); if (!exists) { context.read().setTheme(AppThemeType.doodle); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); StorageService.instance.stopHeartbeat(); // <-- Assicurati di fermarlo _cleanupGhostRoom(); _linkSubscription?.cancel(); _favoritesSubscription?.cancel(); _invitesSubscription?.cancel(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { // --- L'UTENTE TORNA NELL'APP: RIPRENDI IL BATTITO E AGGIORNA SUBITO --- StorageService.instance.syncLeaderboard(); StorageService.instance.startHeartbeat(); _checkClipboardForInvite(); _listenToFavoritesOnline(); } else if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) { // --- L'UTENTE ESCE DALL'APP O LA METTE IN BACKGROUND: FERMA IL BATTITO --- StorageService.instance.stopHeartbeat(); } else if (state == AppLifecycleState.detached) { StorageService.instance.stopHeartbeat(); _cleanupGhostRoom(); } } void _cleanupGhostRoom() { if (_myRoomCode != null && !_roomStarted) { FirebaseFirestore.instance.collection('games').doc(_myRoomCode).delete(); _myRoomCode = null; } } Future _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 _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 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'; if (ModalRoute.of(context)?.isCurrent == true) { _showFavoriteOnlinePopup(name); } } } } } }); } void _showFavoriteOnlinePopup(String name) { if (!mounted) return; // Se lo abbiamo già notificato nell'ultima ora, ignoriamo l'aggiornamento if (_lastOnlineNotifications.containsKey(name)) { if (DateTime.now().difference(_lastOnlineNotifications[name]!).inMinutes < 60) 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 + 85, 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; } } if (ModalRoute.of(context)?.isCurrent == true) { _showInvitePopup(from, code, inviteId); } } } } }); } void _showInvitePopup(String fromName, String roomCode, String inviteId) { final themeType = context.read().currentThemeType; final theme = context.read().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))), ), ], ) ); } void _startDirectChallengeFlow(String targetUid, String targetName) { HomeModals.showChallengeSetupDialog( context, targetName, (int radius, ArenaShape shape, String timeMode) { _executeSendChallenge(targetUid, targetName, radius, shape, timeMode); } ); } Future _executeSendChallenge(String targetUid, String targetName, int radius, ArenaShape shape, String 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, 'shape': shape.name, 'timeMode': timeMode, '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, selectedTimeMode: 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 _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? 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); String hostTimeMode = roomData['timeMode'] is String ? roomData['timeMode'] : (roomData['timeMode'] == true ? 'fixed' : 'relax'); context.read().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode); 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(); 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: () async { _debugTapCount++; // CHEAT LOCALE VIVO SOLO IN DEBUG MODE (ORA CON PIPPO!) if (kDebugMode && playerName.toUpperCase() == 'PIPPO' && _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))) ); } // ACCESSO DASHBOARD else if (_debugTapCount >= 7) { _debugTapCount = 0; if (kDebugMode && playerName.toUpperCase() == 'PIPPO') { Navigator.push(context, MaterialPageRoute(builder: (_) => const AdminScreen())); } else { bool isAdmin = await StorageService.instance.isUserAdmin(); if (isAdmin && mounted) { Navigator.push(context, MaterialPageRoute(builder: (_) => const AdminScreen())); } else if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text("Accesso Negato: Non sei un Amministratore 🛑", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), backgroundColor: Colors.redAccent, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), ) ); } } } }, 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)))), 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)), 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), // --- NUMERO DI VERSIONE APP E BADGE AGGIORNAMENTO --- if (_appVersion.isNotEmpty) Positioned( bottom: MediaQuery.of(context).padding.bottom + 10, left: 20, child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Opacity( opacity: 0.6, child: Text( _appVersion, style: getSharedTextStyle(themeType, TextStyle( color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontSize: 12, fontWeight: FontWeight.bold, letterSpacing: 1.0, )), ), ), if (_updateAvailable) ...[ const SizedBox(width: 15), _PulsingUpdateBadge( themeType: themeType, theme: theme, onTap: _triggerUpdate, ), ] ], ), ), ], ), ); } } // --- NUOVO WIDGET: BADGE AGGIORNAMENTO PULSANTE --- class _PulsingUpdateBadge extends StatefulWidget { final AppThemeType themeType; final ThemeColors theme; final VoidCallback onTap; const _PulsingUpdateBadge({ required this.themeType, required this.theme, required this.onTap, }); @override State<_PulsingUpdateBadge> createState() => _PulsingUpdateBadgeState(); } class _PulsingUpdateBadgeState extends State<_PulsingUpdateBadge> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _scaleAnimation; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 800))..repeat(reverse: true); _scaleAnimation = Tween(begin: 0.95, end: 1.05).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { Color badgeColor = widget.themeType == AppThemeType.doodle ? Colors.red.shade700 : widget.theme.playerRed; Color textColor = widget.themeType == AppThemeType.doodle ? Colors.white : widget.theme.playerRed; Color bgColor = widget.themeType == AppThemeType.doodle ? badgeColor : badgeColor.withOpacity(0.15); return GestureDetector( onTap: widget.onTap, child: ScaleTransition( scale: _scaleAnimation, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: badgeColor, width: 1.5), boxShadow: widget.themeType == AppThemeType.doodle ? [const BoxShadow(color: Colors.black26, offset: Offset(2, 2))] : [BoxShadow(color: badgeColor.withOpacity(0.4), blurRadius: 8)], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.system_update_alt, color: textColor, size: 14), const SizedBox(width: 6), Text( "AGGIORNAMENTO DISPONIBILE", style: getSharedTextStyle(widget.themeType, TextStyle( color: textColor, fontSize: 10, fontWeight: FontWeight.w900, )), ), ], ), ), ), ); } } // ---------------------------------------------------- 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 createState() => _FavoriteOnlinePopupState(); } class _FavoriteOnlinePopupState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _offsetAnimation; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400)); _offsetAnimation = Tween(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(); final themeType = themeManager.currentThemeType; final theme = themeManager.currentColors; Color inkColor = const Color(0xFF111122); return SlideTransition( position: _offsetAnimation, child: Material( color: Colors.transparent, elevation: 100, 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 'package:tetraq/l10n/app_localizations.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/cyber_border.dart'; import '../../widgets/painters.dart'; // <--- ECCO L'IMPORT MANCANTE! import 'lobby_widgets.dart'; class LobbyScreen extends StatefulWidget { final String? initialRoomCode; const LobbyScreen({super.key, this.initialRoomCode}); @override State createState() => _LobbyScreenState(); } class _LobbyScreenState extends State 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; String _timeModeSetting = 'fixed'; 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.detached) { _cleanupGhostRoom(); } } void _cleanupGhostRoom() { if (_myRoomCode != null && !_roomStarted) { FirebaseFirestore.instance.collection('games').doc(_myRoomCode).delete(); _myRoomCode = null; } } Future _createRoom() async { if (_isLoading) return; setState(() => _isLoading = true); try { String code = await _multiplayerService.createGameRoom( _selectedRadius, _playerName, _selectedShape.name, _timeModeSetting, 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 _createRoomAndInvite(String targetUid, String targetName) async { if (_isLoading) return; setState(() => _isLoading = true); try { String code = await _multiplayerService.createGameRoom( _selectedRadius, _playerName, _selectedShape.name, _timeModeSetting, 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 _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? 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); String hostTimeMode = roomData['timeMode'] is String ? roomData['timeMode'] : (roomData['timeMode'] == true ? 'fixed' : 'relax'); context.read().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(); 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)))), ) ) : StreamBuilder( // Interroghiamo Firebase solo per gli UID dei nostri preferiti (max 10 per limiti di Firestore) stream: FirebaseFirestore.instance.collection('leaderboard') .where(FieldPath.documentId, whereIn: favs.map((f) => f['uid']).take(10).toList()) .snapshots(), builder: (context, snapshot) { if (!snapshot.hasData) { return Center(child: CircularProgressIndicator(color: theme.playerBlue)); } // Mappiamo i risultati di Firebase per un accesso rapido Map> liveData = {}; for (var doc in snapshot.data!.docs) { liveData[doc.id] = doc.data() as Map; } return ListView.builder( physics: const BouncingScrollPhysics(), itemCount: favs.length, itemBuilder: (c, i) { String uid = favs[i]['uid']!; String name = favs[i]['name']!; bool isOnline = false; if (liveData.containsKey(uid) && liveData[uid]!['lastActive'] != null) { Timestamp lastActive = liveData[uid]!['lastActive']; int diffInSeconds = DateTime.now().difference(lastActive.toDate()).inSeconds; // Se ha fatto un'azione negli ultimi 3 minuti, lo consideriamo online if (diffInSeconds.abs() < 180) isOnline = true; } return ListTile( leading: Icon( Icons.circle, color: isOnline ? Colors.greenAccent : Colors.redAccent.withOpacity(0.5), size: 14 ), title: Text( name, style: getLobbyTextStyle(themeType, TextStyle( color: isOnline ? theme.text : theme.text.withOpacity(0.5), fontSize: 18, fontWeight: FontWeight.bold )) ), trailing: isOnline ? ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: theme.playerBlue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)) ), onPressed: () { Navigator.pop(ctx); _createRoomAndInvite(uid, name); }, child: Text("SFIDA", style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), ) : Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.grey.withOpacity(0.2), borderRadius: BorderRadius.circular(10) ), child: Text("OFFLINE", style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.grey, fontSize: 12, fontWeight: FontWeight.bold))), ), ); }, ); } ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: Text("CHIUDI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))), ), ], ); } ); } void _showWaitingDialog(String code) { showDialog( context: context, barrierDismissible: false, builder: (dialogContext) { final theme = dialogContext.watch().currentColors; final themeType = dialogContext.read().currentThemeType; final loc = AppLocalizations.of(context)!; Widget dialogContent = Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: theme.playerRed), const SizedBox(height: 25), Text(loc.codeHint, 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( stream: _multiplayerService.listenToRoom(code), builder: (ctx, snapshot) { if (snapshot.hasData && snapshot.data!.exists) { var data = snapshot.data!.data() as Map; if (data['status'] == 'playing') { _roomStarted = true; WidgetsBinding.instance.addPostFrameCallback((_) { Navigator.pop(ctx); context.read().startNewGame(_selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: _selectedShape, timeMode: _timeModeSetting); Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); }); } } return PopScope( canPop: false, onPopInvoked: (didPop) { if (didPop) return; _cleanupGhostRoom(); 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: () { _cleanupGhostRoom(); Navigator.pop(ctx); }, child: Text(loc.btnCancel.toUpperCase(), 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)]))), ), ], ), ), ); }, ); } ); } Widget _buildTimeOption(String label, String sub, String value, ThemeColors theme, AppThemeType type) { bool isSel = value == _timeModeSetting; return Expanded( child: GestureDetector( onTap: () => setState(() => _timeModeSetting = value), child: Container( margin: const EdgeInsets.symmetric(horizontal: 4), height: 50, decoration: BoxDecoration( color: isSel ? Colors.orange.shade600 : (type == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05)), borderRadius: BorderRadius.circular(12), border: Border.all(color: isSel ? Colors.orange.shade800 : (type == AppThemeType.doodle ? const Color(0xFF111122) : Colors.white24), width: isSel ? 2 : 1.5), boxShadow: isSel && type != AppThemeType.doodle ? [BoxShadow(color: Colors.orange.withOpacity(0.5), blurRadius: 8)] : [], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(label, style: getLobbyTextStyle(type, TextStyle(color: isSel ? Colors.white : (type == AppThemeType.doodle ? const Color(0xFF111122) : theme.text), fontWeight: FontWeight.w900, fontSize: 13))), if (sub.isNotEmpty) Text(sub, style: getLobbyTextStyle(type, TextStyle(color: isSel ? Colors.white70 : (type == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.5)), fontWeight: FontWeight.bold, fontSize: 8))), ], ), ), ), ); } @override Widget build(BuildContext context) { final themeManager = context.watch(); final themeType = themeManager.currentThemeType; final theme = themeManager.currentColors; 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'; bool isChaosUnlocked = StorageService.instance.playerLevel >= 7; 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); } else if (themeType == AppThemeType.arcade) { panelBackgroundColor = Colors.black.withOpacity(0.4); } 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(loc.roomSettings, 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(loc.arenaShape, 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(loc.arenaSize, 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(loc.timeAndOptions, 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: [ _buildTimeOption('PRO', '-2s A PARTITA', 'dynamic', theme, themeType), _buildTimeOption('10s', 'FISSO', 'fixed', theme, themeType), _buildTimeOption('RELAX', 'INFINITO', 'relax', theme, themeType), ], ), 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(loc.onlineTitle.toUpperCase(), 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: loc.btnStart.toUpperCase(), color: theme.playerRed, onTap: _createRoom, theme: theme, themeType: themeType), ), const SizedBox(width: 10), Expanded( child: NeonActionButton(label: loc.btnCancel.toUpperCase(), color: Colors.grey.shade600, onTap: () => setState(() => _isCreatingRoom = false), theme: theme, themeType: themeType), ), ], ), ], ) : Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ NeonActionButton(label: loc.createMatch.toUpperCase(), 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(loc.wordOr.toUpperCase(), 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: loc.codeHint.toUpperCase(), 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: loc.joinMatch.toUpperCase(), 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(loc.publicLobbyTitle.toUpperCase(), 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( 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(loc.emptyLobbyMsg, 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; 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(loc.emptyLobbyMsg, 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)['createdAt'] as Timestamp?; Timestamp? tB = (b.data() as Map)['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 host = data['hostName'] ?? 'Guest'; int r = data['radius'] ?? 4; String shapeStr = data['shape'] ?? 'classic'; String tMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax'); String prettyTime = "10s"; if (tMode == 'relax') prettyTime = "Relax"; else if (tMode == 'dynamic') prettyTime = "PRO"; 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("${loc.roomOf} $host", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 18))), const SizedBox(height: 6), Text("Raggio: $r • $prettyShape • $prettyTime", 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(loc.btnEnter.toUpperCase(), 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), ], ), ); } } 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; } // =========================================================================== // 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 createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { @override Widget build(BuildContext context) { final themeManager = context.watch(); 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(); 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 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 createState() => _AnimatedCyberBorderState(); } class _AnimatedCyberBorderState extends State 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().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'; import 'painters.dart'; class GameOverDialog extends StatelessWidget { const GameOverDialog({super.key}); @override Widget build(BuildContext context) { final game = context.read(); final themeManager = context.read(); final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType; Color inkColor = const Color(0xFF111122); 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 = themeType == AppThemeType.doodle ? inkColor : theme.text; } Widget dialogContent = Column( mainAxisSize: MainAxisSize.min, children: [ Text(winnerText, textAlign: TextAlign.center, style: getSharedTextStyle(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: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15), border: themeType == AppThemeType.doodle ? Border.all(color: inkColor.withOpacity(0.3), width: 1.5) : null, ), child: FittedBox( fit: BoxFit.scaleDown, child: Row( mainAxisSize: MainAxisSize.min, children: [ Text("$nameRed: $red", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))), Text(" - ", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, color: themeType == AppThemeType.doodle ? inkColor : theme.text))), Text("$nameBlue: $blue", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))), ], ), ), ), if (game.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: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, width: 1.5), boxShadow: (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : [], ), child: Text("+ ${game.lastMatchXP} XP", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))), ), ], if (game.isVsCPU) ...[ const SizedBox(height: 15), Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.7) : theme.text.withOpacity(0.7)))), ], if (game.isOnline) ...[ const SizedBox(height: 20), if (game.rematchRequested && !game.opponentWantsRematch) Text("In attesa di $nameBlue...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.orange.shade700 : Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))), if (game.opponentWantsRematch && !game.rematchRequested) Text("$nameBlue vuole la rivincita!", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.bold))), if (game.rematchRequested && game.opponentWantsRematch) Text("Avvio nuova partita...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade800 : Colors.green, fontWeight: FontWeight.bold))), ], // --- SEZIONE LEVEL UP E ROADMAP DINAMICA --- if (game.hasLeveledUp && game.unlockedRewards.isNotEmpty) ...[ const SizedBox(height: 30), Divider(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.3) : theme.text.withOpacity(0.2)), const SizedBox(height: 15), Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20), decoration: BoxDecoration( color: themeType == AppThemeType.doodle ? Colors.amber.withOpacity(0.1) : Colors.amber.withOpacity(0.2), borderRadius: BorderRadius.circular(30), border: Border.all(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, width: 2) ), child: Text("🎉 LIVELLO ${game.newlyReachedLevel}! 🎉", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, fontWeight: FontWeight.w900, fontSize: 18))), ), const SizedBox(height: 15), ...game.unlockedRewards.map((reward) { Color rewardColor = themeType == AppThemeType.doodle ? (reward['color'] as Color).withOpacity(0.8) : reward['color']; return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: rewardColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: rewardColor.withOpacity(0.5), width: 1.5), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: rewardColor.withOpacity(0.2), shape: BoxShape.circle, ), child: Icon(reward['icon'], color: rewardColor, size: 28), ), const SizedBox(width: 15), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(reward['title'], style: getSharedTextStyle(themeType, TextStyle(color: rewardColor, fontWeight: FontWeight.w900, fontSize: 16))), const SizedBox(height: 4), Text(reward['desc'], style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.9) : theme.text.withOpacity(0.9), fontSize: 12, height: 1.3))), ], ) ) ] ), ); }), ], const SizedBox(height: 30), // --- BOTTONI AZIONE --- Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (playerBeatCPU) _buildPrimaryButton( "PROSSIMO LIVELLO ➔", winnerColor, themeType, inkColor, () { Navigator.pop(context); game.increaseLevelAndRestart(); }, ) else if (game.isOnline) _buildPrimaryButton( game.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", game.rematchRequested ? Colors.grey : (winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor), themeType, inkColor, game.rematchRequested ? () {} : () => game.requestRematch(), ) else _buildPrimaryButton( "RIGIOCA", winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor, themeType, inkColor, () { Navigator.pop(context); game.startNewGame(game.board.radius, vsCPU: game.isVsCPU); }, ), const SizedBox(height: 12), _buildSecondaryButton( "TORNA AL MENU", themeType, inkColor, theme, () { if (game.isOnline) { game.disconnectOnlineGame(); } Navigator.pop(context); Navigator.pop(context); }, ), ], ) ], ); if (themeType == AppThemeType.doodle) { dialogContent = Transform.rotate( angle: 0.015, child: CustomPaint( painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 500), child: Padding( padding: const EdgeInsets.all(25.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2))), const SizedBox(height: 20), dialogContent, ], ), ), ), ); } else { dialogContent = Container( padding: const EdgeInsets.all(25.0), decoration: BoxDecoration( color: theme.background, borderRadius: BorderRadius.circular(20), border: Border.all(color: winnerColor.withOpacity(0.5), width: 2), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: theme.text))), const SizedBox(height: 20), dialogContent, ], ), ); } return Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), child: dialogContent, ); } Widget _buildPrimaryButton(String label, Color color, AppThemeType themeType, Color inkColor, VoidCallback onTap) { if (themeType == AppThemeType.doodle) { return GestureDetector( onTap: onTap, child: CustomPaint( painter: DoodleBackgroundPainter(fillColor: color, strokeColor: inkColor, seed: label.length * 7), child: Container( height: 55, alignment: Alignment.center, child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 1.5))), ), ), ); } return ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: color, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5, ), onPressed: onTap, child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5))), ); } Widget _buildSecondaryButton(String label, AppThemeType themeType, Color inkColor, ThemeColors theme, VoidCallback onTap) { if (themeType == AppThemeType.doodle) { return GestureDetector( onTap: onTap, child: CustomPaint( painter: DoodleBackgroundPainter(fillColor: Colors.transparent, strokeColor: inkColor.withOpacity(0.5), seed: label.length * 3), child: Container( height: 55, alignment: Alignment.center, child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 1.5))), ), ), ); } return OutlinedButton( style: OutlinedButton.styleFrom( foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), ), onPressed: onTap, child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))), ); } } // =========================================================================== // 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; }