749 lines
No EOL
38 KiB
Dart
749 lines
No EOL
38 KiB
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<LobbyScreen> createState() => _LobbyScreenState();
|
|
}
|
|
|
|
class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
|
|
final MultiplayerService _multiplayerService = MultiplayerService();
|
|
late TextEditingController _codeController;
|
|
|
|
bool _isLoading = false;
|
|
String? _myRoomCode;
|
|
String _playerName = '';
|
|
|
|
bool _isCreatingRoom = false;
|
|
|
|
int _selectedRadius = 4;
|
|
ArenaShape _selectedShape = ArenaShape.classic;
|
|
|
|
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<void> _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<void> _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<void> _joinRoomByCode(String code) async {
|
|
if (_isLoading) return;
|
|
FocusScope.of(context).unfocus();
|
|
|
|
code = code.trim().toUpperCase();
|
|
if (code.isEmpty || code.length != 5) { _showError("Inserisci un codice valido di 5 caratteri."); return; }
|
|
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
Map<String, dynamic>? roomData = await _multiplayerService.joinGameRoom(code, _playerName);
|
|
|
|
if (!mounted) return;
|
|
setState(() => _isLoading = false);
|
|
|
|
if (roomData != null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza trovata! Partita in avvio..."), backgroundColor: Colors.green));
|
|
|
|
int hostRadius = roomData['radius'] ?? 4;
|
|
String shapeStr = roomData['shape'] ?? 'classic';
|
|
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
|
|
|
|
String hostTimeMode = roomData['timeMode'] is String ? roomData['timeMode'] : (roomData['timeMode'] == true ? 'fixed' : 'relax');
|
|
|
|
context.read<GameController>().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode);
|
|
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
|
|
} else {
|
|
_showError("Stanza non trovata, piena o partita già iniziata.");
|
|
}
|
|
} catch (e) {
|
|
if (mounted) { setState(() => _isLoading = false); _showError("Errore di connessione: $e"); }
|
|
}
|
|
}
|
|
|
|
void _showError(String message) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message, style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red)); }
|
|
|
|
void _showFavoritesDialogForCreation() {
|
|
final favs = StorageService.instance.favorites;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) {
|
|
final themeManager = ctx.watch<ThemeManager>();
|
|
final theme = themeManager.currentColors;
|
|
final themeType = themeManager.currentThemeType;
|
|
|
|
return AlertDialog(
|
|
backgroundColor: theme.background,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
title: Text("I TUOI PREFERITI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold))),
|
|
content: Container(
|
|
width: double.maxFinite,
|
|
height: 300,
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: theme.playerRed, width: 2),
|
|
borderRadius: BorderRadius.circular(10)
|
|
),
|
|
child: favs.isEmpty
|
|
? Center(child: Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Text("Non hai ancora aggiunto nessun preferito dalla Classifica!", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6)))),
|
|
))
|
|
: ListView.builder(
|
|
itemCount: favs.length,
|
|
itemBuilder: (c, i) {
|
|
return ListTile(
|
|
title: Text(favs[i]['name']!, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontSize: 18, fontWeight: FontWeight.bold))),
|
|
trailing: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
|
|
onPressed: () {
|
|
Navigator.pop(ctx);
|
|
_createRoomAndInvite(favs[i]['uid']!, favs[i]['name']!);
|
|
},
|
|
child: Text("SFIDA", style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(ctx), child: Text("CHIUDI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.playerRed))))
|
|
],
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
void _showWaitingDialog(String code) {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (dialogContext) {
|
|
final theme = dialogContext.watch<ThemeManager>().currentColors;
|
|
final themeType = dialogContext.read<ThemeManager>().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<DocumentSnapshot>(
|
|
stream: _multiplayerService.listenToRoom(code),
|
|
builder: (ctx, snapshot) {
|
|
if (snapshot.hasData && snapshot.data!.exists) {
|
|
var data = snapshot.data!.data() as Map<String, dynamic>;
|
|
if (data['status'] == 'playing') {
|
|
_roomStarted = true;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
Navigator.pop(ctx);
|
|
context.read<GameController>().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<ThemeManager>();
|
|
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('10s', 'FISSO', 'fixed', theme, themeType),
|
|
_buildTimeOption('RELAX', 'INFINITO', 'relax', theme, themeType),
|
|
_buildTimeOption('DINAMICO', '-2s', 'dynamic', 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<QuerySnapshot>(
|
|
stream: _multiplayerService.getPublicRooms(),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return Padding(padding: const EdgeInsets.all(20), child: Center(child: CircularProgressIndicator(color: theme.playerBlue)));
|
|
}
|
|
|
|
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 40.0),
|
|
child: Center(child: Text(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<String, dynamic>;
|
|
if (data['isPublic'] != true) return false;
|
|
if (data['hostUid'] != null && data['hostUid'] == myUid) return false;
|
|
|
|
Timestamp? createdAt = data['createdAt'] as Timestamp?;
|
|
if (createdAt != null) {
|
|
int ageInMinutes = now.difference(createdAt.toDate()).inMinutes;
|
|
if (ageInMinutes > 15) {
|
|
FirebaseFirestore.instance.collection('games').doc(doc.id).delete();
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}).toList();
|
|
|
|
if (docs.isEmpty) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 40.0),
|
|
child: Center(child: Text(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<String, dynamic>)['createdAt'] as Timestamp?;
|
|
Timestamp? tB = (b.data() as Map<String, dynamic>)['createdAt'] as Timestamp?;
|
|
if (tA == null || tB == null) return 0;
|
|
return tB.compareTo(tA);
|
|
});
|
|
|
|
return ListView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
padding: EdgeInsets.zero,
|
|
itemCount: docs.length,
|
|
itemBuilder: (context, index) {
|
|
var doc = docs[index];
|
|
var data = doc.data() as Map<String, dynamic>;
|
|
String host = data['hostName'] ?? '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 = "Dinamico";
|
|
|
|
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;
|
|
} |