tetraq/lib/ui/home/dialog.dart
2026-03-21 11:00:05 +01:00

541 lines
No EOL
28 KiB
Dart

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