1217 lines
37 KiB
Dart
1217 lines
37 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:farolero/l10n/generated/app_localizations.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_animate/flutter_animate.dart';
|
|
|
|
import '../modelos/gamificacion_usuario.dart';
|
|
import '../modelos/jugador.dart';
|
|
import '../modelos/partida.dart';
|
|
import 'tema_app.dart';
|
|
import 'componentes_farolero.dart';
|
|
|
|
class ResultadoRondaFarolero extends StatelessWidget {
|
|
final ResultadoVotacion resultado;
|
|
final List<Jugador> jugadores;
|
|
final String? mensaje;
|
|
final Widget? acciones;
|
|
|
|
const ResultadoRondaFarolero({
|
|
super.key,
|
|
required this.resultado,
|
|
required this.jugadores,
|
|
this.mensaje,
|
|
this.acciones,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final color =
|
|
resultado.eraImpostor ? TemaApp.colorVerde : TemaApp.colorAcento;
|
|
|
|
return Column(
|
|
children: [
|
|
const SizedBox(height: 8),
|
|
Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
SizedBox(
|
|
height: 188,
|
|
width: double.infinity,
|
|
child: CustomPaint(
|
|
painter: HeroCinematicoResultadoPainter(color: color),
|
|
),
|
|
),
|
|
Image.asset(
|
|
'assets/ui/generated/meta/result_verdict_art.webp',
|
|
height: 150,
|
|
fit: BoxFit.contain,
|
|
).animate().scale(
|
|
duration: 520.ms,
|
|
curve: Curves.elasticOut,
|
|
begin: const Offset(0.78, 0.78),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
resultado.eliminadoNombre,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
|
fontSize: 36,
|
|
fontWeight: FontWeight.w900,
|
|
shadows: [
|
|
Shadow(color: color.withValues(alpha: 0.55), blurRadius: 18),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_BadgeResultadoRonda(
|
|
texto: resultado.eraImpostor ? l10n.wasImpostor : l10n.wasInnocent,
|
|
color: color,
|
|
),
|
|
if (mensaje != null && mensaje!.trim().isNotEmpty) ...[
|
|
const SizedBox(height: 14),
|
|
PanelResultadoFarolero(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Text(
|
|
mensaje!,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 22),
|
|
DetalleVotosFarolero(resultado: resultado, jugadores: jugadores),
|
|
if (acciones != null) ...[
|
|
const SizedBox(height: 24),
|
|
acciones!,
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class DetalleVotosFarolero extends StatelessWidget {
|
|
final ResultadoVotacion resultado;
|
|
final List<Jugador> jugadores;
|
|
|
|
const DetalleVotosFarolero({
|
|
super.key,
|
|
required this.resultado,
|
|
required this.jugadores,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final jugadoresPorId = {for (final jugador in jugadores) jugador.id: jugador};
|
|
final conteo = <String, int>{};
|
|
for (final votadoId in resultado.votos.values) {
|
|
conteo[votadoId] = (conteo[votadoId] ?? 0) + 1;
|
|
}
|
|
final maxVotos = conteo.values.isEmpty
|
|
? 1
|
|
: conteo.values.reduce((a, b) => a > b ? a : b);
|
|
final ranking = conteo.entries.toList()
|
|
..sort((a, b) => b.value.compareTo(a.value));
|
|
|
|
return PanelResultadoFarolero(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
IconoFarolero(Icons.bar_chart, color: TemaApp.colorNaranja),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
l10n.votesThisRound,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
...ranking.map((entry) {
|
|
final jugador = jugadoresPorId[entry.key];
|
|
return BarraVotosFarolero(
|
|
nombre: jugador?.nombre ?? '?',
|
|
votos: entry.value,
|
|
total: maxVotos,
|
|
destacado: entry.key == resultado.eliminadoId,
|
|
);
|
|
}),
|
|
const Divider(height: 26),
|
|
...resultado.votos.entries.map((entry) {
|
|
final votante = jugadoresPorId[entry.key]?.nombre ?? '?';
|
|
final votado = jugadoresPorId[entry.value]?.nombre ?? '?';
|
|
final fueAlEliminado = entry.value == resultado.eliminadoId;
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
children: [
|
|
IconoFarolero(
|
|
fueAlEliminado ? Icons.how_to_vote : Icons.arrow_forward,
|
|
size: 18,
|
|
color: fueAlEliminado
|
|
? TemaApp.colorAcento
|
|
: TemaApp.colorTextoSecundario,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'$votante → $votado',
|
|
style: TextStyle(
|
|
color: fueAlEliminado
|
|
? TemaApp.colorTexto
|
|
: TemaApp.colorTextoSecundario,
|
|
fontWeight: fueAlEliminado
|
|
? FontWeight.bold
|
|
: FontWeight.normal,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
).animate().fadeIn(delay: 160.ms).slideY(begin: 0.08);
|
|
}
|
|
}
|
|
|
|
class BarraVotosFarolero extends StatelessWidget {
|
|
final String nombre;
|
|
final int votos;
|
|
final int total;
|
|
final bool destacado;
|
|
|
|
const BarraVotosFarolero({
|
|
super.key,
|
|
required this.nombre,
|
|
required this.votos,
|
|
required this.total,
|
|
required this.destacado,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final color = destacado ? TemaApp.colorAcento : TemaApp.colorNaranja;
|
|
final proporcion = total == 0 ? 0.0 : votos / total;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
nombre,
|
|
style: TextStyle(
|
|
fontWeight: destacado ? FontWeight.bold : FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
'$votos',
|
|
style: TextStyle(color: color, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(999),
|
|
child: LinearProgressIndicator(
|
|
value: proporcion.clamp(0.0, 1.0).toDouble(),
|
|
minHeight: 12,
|
|
backgroundColor: Colors.black.withValues(alpha: 0.42),
|
|
valueColor: AlwaysStoppedAnimation(color),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HeroFinalPartidaFarolero extends StatelessWidget {
|
|
final String encabezado;
|
|
final String titulo;
|
|
final IconData icono;
|
|
final Color color;
|
|
|
|
const HeroFinalPartidaFarolero({
|
|
super.key,
|
|
required this.encabezado,
|
|
required this.titulo,
|
|
required this.icono,
|
|
required this.color,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final apertura = String.fromCharCode(0x00A1);
|
|
final tituloLimpio = titulo
|
|
.replaceAll(apertura, '')
|
|
.replaceAll('!', '')
|
|
.trim()
|
|
.toUpperCase();
|
|
|
|
return Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
SizedBox(
|
|
height: 520,
|
|
width: double.infinity,
|
|
child: CustomPaint(painter: HeroCinematicoResultadoPainter(color: color)),
|
|
),
|
|
Column(
|
|
children: [
|
|
Text(
|
|
encabezado,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontSize: 38,
|
|
fontWeight: FontWeight.w900,
|
|
shadows: [
|
|
Shadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.55),
|
|
blurRadius: 18,
|
|
),
|
|
],
|
|
),
|
|
).animate().fadeIn(duration: 260.ms).slideY(begin: -0.18),
|
|
const SizedBox(height: 58),
|
|
Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Image.asset(
|
|
'assets/ui/generated/final_rewards/cinematic_burst.webp',
|
|
width: 260,
|
|
height: 260,
|
|
fit: BoxFit.contain,
|
|
),
|
|
Container(
|
|
width: 154,
|
|
height: 154,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [
|
|
color.withValues(alpha: 0.36),
|
|
const Color(0xFF111116),
|
|
Colors.black.withValues(alpha: 0.88),
|
|
],
|
|
),
|
|
border: Border.all(color: TemaApp.colorDorado, width: 4),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.75),
|
|
blurRadius: 52,
|
|
spreadRadius: 7,
|
|
),
|
|
BoxShadow(
|
|
color: color.withValues(alpha: 0.62),
|
|
blurRadius: 36,
|
|
spreadRadius: 2,
|
|
),
|
|
],
|
|
),
|
|
child: IconoResultadoPremiumFarolero(icono: icono),
|
|
),
|
|
],
|
|
)
|
|
.animate()
|
|
.scale(
|
|
begin: const Offset(0.55, 0.55),
|
|
duration: 520.ms,
|
|
curve: Curves.elasticOut,
|
|
)
|
|
.shimmer(delay: 700.ms, duration: 1500.ms),
|
|
const SizedBox(height: 14),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Text(
|
|
'$apertura$tituloLimpio!',
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
|
color: color,
|
|
fontSize: 32,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 1.0,
|
|
shadows: [
|
|
Shadow(
|
|
color: color.withValues(alpha: 0.90),
|
|
blurRadius: 24,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
).animate().fadeIn(delay: 180.ms).slideY(begin: 0.25),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class IconoResultadoPremiumFarolero extends StatelessWidget {
|
|
final IconData icono;
|
|
|
|
const IconoResultadoPremiumFarolero({super.key, required this.icono});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (icono != Icons.theater_comedy) {
|
|
return IconoFarolero(icono, size: 82, color: TemaApp.colorDorado);
|
|
}
|
|
return Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Transform.translate(
|
|
offset: const Offset(-18, 12),
|
|
child: Transform.rotate(
|
|
angle: -0.10,
|
|
child: IconoFarolero(
|
|
Icons.mood,
|
|
size: 66,
|
|
color: TemaApp.colorDorado.withValues(alpha: 0.98),
|
|
),
|
|
),
|
|
),
|
|
Transform.translate(
|
|
offset: const Offset(20, -13),
|
|
child: Transform.rotate(
|
|
angle: 0.12,
|
|
child: IconoFarolero(
|
|
Icons.sentiment_dissatisfied,
|
|
size: 70,
|
|
color: TemaApp.colorDorado.withValues(alpha: 0.98),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class TarjetaProgresoGamificacionPremium extends StatelessWidget {
|
|
final ProgresoGamificacionUsuario progreso;
|
|
|
|
const TarjetaProgresoGamificacionPremium({
|
|
super.key,
|
|
required this.progreso,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final nuevas = progreso.nuevasMedallas;
|
|
final antes = progreso.antes.fuego.clamp(0, 100);
|
|
final despues = progreso.despues.fuego.clamp(0, 100);
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
return PanelResultadoFarolero(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const _IconoFuegoRecompensa(),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Text(
|
|
l10n.matchRewards.toUpperCase(),
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 1.1,
|
|
height: 1.05,
|
|
),
|
|
),
|
|
),
|
|
_DeltaFuegoPremium(valor: progreso.incrementoFuego),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
BarraFuegoPremiumFarolero(antes: antes, despues: despues),
|
|
const SizedBox(height: 20),
|
|
if (nuevas.isEmpty)
|
|
Text(
|
|
l10n.noNewMedalsKeepFire,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
color: TemaApp.colorTextoSecundario,
|
|
height: 1.35,
|
|
),
|
|
)
|
|
else ...[
|
|
Text(
|
|
l10n.newMedals.toUpperCase(),
|
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: 1.5,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 12,
|
|
runSpacing: 12,
|
|
alignment: WrapAlignment.center,
|
|
children: [
|
|
for (final id in nuevas) MedallaDesbloqueadaPremium(id: id),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
).animate().fadeIn(delay: 250.ms).slideY(begin: 0.12);
|
|
}
|
|
}
|
|
|
|
class TarjetaRecompensaCargandoPremium extends StatelessWidget {
|
|
const TarjetaRecompensaCargandoPremium({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PanelResultadoFarolero(
|
|
child: Row(
|
|
children: [
|
|
const SizedBox(
|
|
width: 28,
|
|
height: 28,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 3,
|
|
color: TemaApp.colorNaranja,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
AppLocalizations.of(context)!.calculatingRewards,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PanelResultadoFarolero extends StatelessWidget {
|
|
final Widget child;
|
|
final EdgeInsetsGeometry padding;
|
|
|
|
const PanelResultadoFarolero({
|
|
super.key,
|
|
required this.child,
|
|
this.padding = const EdgeInsets.all(24),
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(32),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: padding,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
const Color(0xFF101A27).withValues(alpha: 0.96),
|
|
const Color(0xFF1B0E24).withValues(alpha: 0.94),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(32),
|
|
border: Border.all(color: TemaApp.colorDorado.withValues(alpha: 0.58)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.25),
|
|
blurRadius: 44,
|
|
offset: const Offset(0, 22),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.50),
|
|
blurRadius: 28,
|
|
offset: const Offset(0, 14),
|
|
),
|
|
],
|
|
),
|
|
foregroundDecoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(32),
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.white.withValues(alpha: 0.06),
|
|
Colors.transparent,
|
|
TemaApp.colorDorado.withValues(alpha: 0.04),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
),
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class BarraFuegoPremiumFarolero extends StatelessWidget {
|
|
final int antes;
|
|
final int despues;
|
|
|
|
const BarraFuegoPremiumFarolero({
|
|
super.key,
|
|
required this.antes,
|
|
required this.despues,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: antes / 100, end: despues / 100),
|
|
duration: const Duration(milliseconds: 1300),
|
|
curve: Curves.easeOutCubic,
|
|
builder: (context, value, child) {
|
|
final normalizado = value.clamp(0.0, 1.0).toDouble();
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Image.asset(
|
|
'assets/ui/generated/actions/action_fire_badge.webp',
|
|
width: 22,
|
|
height: 22,
|
|
fit: BoxFit.contain,
|
|
filterQuality: FilterQuality.medium,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'${AppLocalizations.of(context)!.fireLabel} '
|
|
'${(normalizado * 100).round()}%',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
height: 38,
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withValues(alpha: 0.72),
|
|
borderRadius: BorderRadius.circular(999),
|
|
border: Border.all(
|
|
color: TemaApp.colorDorado.withValues(alpha: 0.38),
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.24),
|
|
blurRadius: 18,
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(999),
|
|
child: Stack(
|
|
children: [
|
|
FractionallySizedBox(
|
|
widthFactor: normalizado,
|
|
child: Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Color(0xFFE53935),
|
|
TemaApp.colorNaranja,
|
|
TemaApp.colorDorado,
|
|
Color(0xFFFFECBE),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Positioned.fill(
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.white.withValues(alpha: 0.32),
|
|
Colors.transparent,
|
|
Colors.black.withValues(alpha: 0.18),
|
|
],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class MedallaDesbloqueadaPremium extends StatelessWidget {
|
|
final String id;
|
|
|
|
const MedallaDesbloqueadaPremium({super.key, required this.id});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final medalla = EstadisticasPerfilUsuario.catalogoMedallas[id];
|
|
if (medalla == null) return const SizedBox.shrink();
|
|
return Container(
|
|
width: 94,
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withValues(alpha: 0.28),
|
|
borderRadius: BorderRadius.circular(18),
|
|
border: Border.all(color: TemaApp.colorDorado.withValues(alpha: 0.48)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Image.asset(
|
|
'assets/rewards/medal_unlock_burst.webp',
|
|
width: 82,
|
|
height: 82,
|
|
fit: BoxFit.cover,
|
|
),
|
|
SizedBox(
|
|
width: 70,
|
|
height: 70,
|
|
child: CustomPaint(
|
|
painter: MiniBurstFaroleroPainter(color: TemaApp.colorDorado),
|
|
),
|
|
),
|
|
Image.asset(
|
|
medalla.assetPath,
|
|
width: 58,
|
|
height: 58,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
Text(medalla.emoji, style: const TextStyle(fontSize: 32)),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
medalla.nombre,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
|
color: TemaApp.colorTexto,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
.animate()
|
|
.scale(duration: 520.ms, curve: Curves.elasticOut)
|
|
.shimmer(delay: 650.ms, duration: 1200.ms);
|
|
}
|
|
}
|
|
|
|
class TarjetaSecretoPremium extends StatelessWidget {
|
|
final String palabra;
|
|
final String categoria;
|
|
|
|
const TarjetaSecretoPremium({
|
|
super.key,
|
|
required this.palabra,
|
|
required this.categoria,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
return PanelResultadoFarolero(
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
l10n.theSecretWordWas,
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
Text(
|
|
palabra.toUpperCase(),
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
|
color: TemaApp.colorNaranja,
|
|
fontSize: 42,
|
|
fontWeight: FontWeight.w900,
|
|
shadows: [
|
|
Shadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.55),
|
|
blurRadius: 18,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
l10n.categoryLabel(categoria),
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: TemaApp.colorTextoSecundario,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
).animate().fadeIn(delay: 380.ms).slideY(begin: 0.1);
|
|
}
|
|
}
|
|
|
|
class TarjetaImpostoresPremium extends StatelessWidget {
|
|
final String titulo;
|
|
final List<Jugador> impostores;
|
|
|
|
const TarjetaImpostoresPremium({
|
|
super.key,
|
|
required this.titulo,
|
|
required this.impostores,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PanelResultadoFarolero(
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
IconoFarolero(Icons.theater_comedy, color: TemaApp.colorAcento),
|
|
const SizedBox(width: 8),
|
|
Flexible(
|
|
child: Text(
|
|
titulo,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
...impostores.map(
|
|
(jugador) => Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
IconoFarolero(
|
|
Icons.theater_comedy,
|
|
size: 20,
|
|
color: TemaApp.colorAcento,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Flexible(
|
|
child: Text(
|
|
jugador.nombre,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.titleLarge
|
|
?.copyWith(color: TemaApp.colorAcento),
|
|
),
|
|
),
|
|
if (jugador.eliminado) ...[
|
|
const SizedBox(width: 8),
|
|
IconoFarolero(
|
|
Icons.close,
|
|
size: 16,
|
|
color: TemaApp.colorTextoSecundario,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
).animate().fadeIn(delay: 450.ms);
|
|
}
|
|
}
|
|
|
|
class TarjetaHistorialVotosPremium extends StatelessWidget {
|
|
final List<ResultadoVotacion> historial;
|
|
final List<Jugador> jugadores;
|
|
|
|
const TarjetaHistorialVotosPremium({
|
|
super.key,
|
|
required this.historial,
|
|
required this.jugadores,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final jugadoresPorId = {for (final jugador in jugadores) jugador.id: jugador};
|
|
return PanelResultadoFarolero(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.votingHistory,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
...historial.asMap().entries.map((entrada) {
|
|
final ronda = entrada.key + 1;
|
|
final resultado = entrada.value;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.roundElimination(ronda, resultado.eliminadoNombre),
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: resultado.eraImpostor
|
|
? TemaApp.colorVerde
|
|
: TemaApp.colorAcento,
|
|
),
|
|
),
|
|
...resultado.votos.entries.map((voto) {
|
|
final votante = jugadoresPorId[voto.key]?.nombre ?? '?';
|
|
final votado = jugadoresPorId[voto.value]?.nombre ?? '?';
|
|
return Text(
|
|
' $votante → $votado',
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class EscenarioFinPartidaFaroleroPainter extends CustomPainter {
|
|
final Color color;
|
|
|
|
const EscenarioFinPartidaFaroleroPainter({required this.color});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()..isAntiAlias = true;
|
|
|
|
paint.shader = const LinearGradient(
|
|
colors: [Color(0xFF050B14), Color(0xFF091322), Color(0xFF16091F)],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
).createShader(Offset.zero & size);
|
|
canvas.drawRect(Offset.zero & size, paint);
|
|
|
|
paint.shader = RadialGradient(
|
|
colors: [
|
|
color.withValues(alpha: 0.28),
|
|
TemaApp.colorNaranja.withValues(alpha: 0.12),
|
|
Colors.transparent,
|
|
],
|
|
).createShader(
|
|
Rect.fromCircle(
|
|
center: Offset(size.width * 0.5, size.height * 0.30),
|
|
radius: size.width * 0.86,
|
|
),
|
|
);
|
|
canvas.drawCircle(
|
|
Offset(size.width * 0.5, size.height * 0.30),
|
|
size.width * 0.86,
|
|
paint,
|
|
);
|
|
|
|
paint.shader = null;
|
|
paint.color = Colors.black.withValues(alpha: 0.36);
|
|
final base = size.height * 0.78;
|
|
final path = Path()
|
|
..moveTo(0, size.height)
|
|
..lineTo(0, base)
|
|
..lineTo(size.width * 0.16, base - 42)
|
|
..lineTo(size.width * 0.30, base - 12)
|
|
..lineTo(size.width * 0.48, base - 60)
|
|
..lineTo(size.width * 0.66, base - 26)
|
|
..lineTo(size.width * 0.82, base - 72)
|
|
..lineTo(size.width, base - 34)
|
|
..lineTo(size.width, size.height)
|
|
..close();
|
|
canvas.drawPath(path, paint);
|
|
|
|
for (var i = 0; i < 52; i++) {
|
|
final x = (i * 71 % math.max(size.width, 1)).toDouble();
|
|
final y = (i * 137 % math.max(size.height, 1)).toDouble();
|
|
final palette = [
|
|
TemaApp.colorDorado,
|
|
TemaApp.colorNaranja,
|
|
TemaApp.colorAcento,
|
|
const Color(0xFFFFF1C7),
|
|
];
|
|
final confettiPaint = Paint()
|
|
..isAntiAlias = true
|
|
..color = palette[i % palette.length].withValues(alpha: 0.72);
|
|
canvas.save();
|
|
canvas.translate(x, y);
|
|
canvas.rotate((i % 9 - 4) * 0.22);
|
|
canvas.drawRRect(
|
|
RRect.fromRectAndRadius(
|
|
Rect.fromCenter(
|
|
center: Offset.zero,
|
|
width: 8 + (i % 4) * 6,
|
|
height: 4 + (i % 3) * 3,
|
|
),
|
|
const Radius.circular(1.5),
|
|
),
|
|
confettiPaint,
|
|
);
|
|
canvas.restore();
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant EscenarioFinPartidaFaroleroPainter oldDelegate) {
|
|
return oldDelegate.color != color;
|
|
}
|
|
}
|
|
|
|
class HeroCinematicoResultadoPainter extends CustomPainter {
|
|
final Color color;
|
|
|
|
const HeroCinematicoResultadoPainter({required this.color});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final center = Offset(size.width / 2, size.height * 0.54);
|
|
final paint = Paint()..isAntiAlias = true;
|
|
|
|
paint.shader = RadialGradient(
|
|
colors: [
|
|
TemaApp.colorNaranja.withValues(alpha: 0.30),
|
|
color.withValues(alpha: 0.14),
|
|
Colors.transparent,
|
|
],
|
|
).createShader(Rect.fromCircle(center: center, radius: size.width * 0.54));
|
|
canvas.drawCircle(center, size.width * 0.54, paint);
|
|
paint.shader = null;
|
|
|
|
for (var i = 0; i < 28; i++) {
|
|
final angle = (math.pi * 2 / 28) * i;
|
|
final inner = Offset(
|
|
center.dx + math.cos(angle - 0.035) * 52,
|
|
center.dy + math.sin(angle - 0.035) * 52,
|
|
);
|
|
final outer = Offset(
|
|
center.dx + math.cos(angle) * size.width * 0.58,
|
|
center.dy + math.sin(angle) * size.width * 0.58,
|
|
);
|
|
final inner2 = Offset(
|
|
center.dx + math.cos(angle + 0.035) * 52,
|
|
center.dy + math.sin(angle + 0.035) * 52,
|
|
);
|
|
paint.color = (i.isEven ? TemaApp.colorDorado : color)
|
|
.withValues(alpha: i.isEven ? 0.16 : 0.09);
|
|
canvas.drawPath(
|
|
Path()
|
|
..moveTo(inner.dx, inner.dy)
|
|
..lineTo(outer.dx, outer.dy)
|
|
..lineTo(inner2.dx, inner2.dy)
|
|
..close(),
|
|
paint,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant HeroCinematicoResultadoPainter oldDelegate) {
|
|
return oldDelegate.color != color;
|
|
}
|
|
}
|
|
|
|
class MiniBurstFaroleroPainter extends CustomPainter {
|
|
final Color color;
|
|
|
|
const MiniBurstFaroleroPainter({required this.color});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final center = Offset(size.width / 2, size.height / 2);
|
|
final paint = Paint()
|
|
..isAntiAlias = true
|
|
..color = color.withValues(alpha: 0.22)
|
|
..strokeCap = StrokeCap.round;
|
|
for (var i = 0; i < 16; i++) {
|
|
final angle = math.pi * 2 * i / 16;
|
|
paint.strokeWidth = i.isEven ? 3 : 1.4;
|
|
canvas.drawLine(
|
|
center,
|
|
Offset(
|
|
center.dx + math.cos(angle) * size.width * 0.48,
|
|
center.dy + math.sin(angle) * size.height * 0.48,
|
|
),
|
|
paint,
|
|
);
|
|
}
|
|
paint
|
|
..style = PaintingStyle.fill
|
|
..shader = RadialGradient(
|
|
colors: [color.withValues(alpha: 0.38), Colors.transparent],
|
|
).createShader(Rect.fromCircle(center: center, radius: size.width * 0.42));
|
|
canvas.drawCircle(center, size.width * 0.42, paint);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant MiniBurstFaroleroPainter oldDelegate) {
|
|
return oldDelegate.color != color;
|
|
}
|
|
}
|
|
|
|
class _BadgeResultadoRonda extends StatelessWidget {
|
|
final String texto;
|
|
final Color color;
|
|
|
|
const _BadgeResultadoRonda({required this.texto, required this.color});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.24),
|
|
borderRadius: BorderRadius.circular(30),
|
|
border: Border.all(color: color),
|
|
boxShadow: [
|
|
BoxShadow(color: color.withValues(alpha: 0.28), blurRadius: 18),
|
|
],
|
|
),
|
|
child: Text(
|
|
texto,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: color,
|
|
),
|
|
),
|
|
).animate().scale(delay: 180.ms, duration: 380.ms);
|
|
}
|
|
}
|
|
|
|
class _IconoFuegoRecompensa extends StatelessWidget {
|
|
const _IconoFuegoRecompensa();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: 52,
|
|
height: 52,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
TemaApp.colorNaranja.withValues(alpha: 0.95),
|
|
TemaApp.colorDorado.withValues(alpha: 0.78),
|
|
],
|
|
begin: Alignment.bottomLeft,
|
|
end: Alignment.topRight,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.42),
|
|
blurRadius: 22,
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(7),
|
|
child: Image.asset(
|
|
'assets/ui/generated/actions/action_fire_badge.webp',
|
|
fit: BoxFit.contain,
|
|
filterQuality: FilterQuality.medium,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DeltaFuegoPremium extends StatelessWidget {
|
|
final int valor;
|
|
|
|
const _DeltaFuegoPremium({required this.valor});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final texto = valor >= 0 ? '+$valor' : '$valor';
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
TemaApp.colorNaranja.withValues(alpha: 0.28),
|
|
TemaApp.colorDorado.withValues(alpha: 0.14),
|
|
],
|
|
),
|
|
borderRadius: BorderRadius.circular(999),
|
|
border: Border.all(color: TemaApp.colorNaranja),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.28),
|
|
blurRadius: 18,
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Image.asset(
|
|
'assets/ui/generated/actions/action_fire_badge.webp',
|
|
width: 26,
|
|
height: 26,
|
|
fit: BoxFit.contain,
|
|
filterQuality: FilterQuality.medium,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
texto,
|
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
).animate().scale(delay: 520.ms, duration: 420.ms, curve: Curves.elasticOut);
|
|
}
|
|
}
|