Fixes
- SnapshotPartidaOnline no longer serializes esImpostor unless the game is
over. Every phase change was broadcasting the impostor roles in clear to
all clients.
- The impostor clue travels in its own field and only when the game enables
it. It used to be sent as the category key ("todas" when no category was
picked) and was overwritten by each phase snapshot.
- Eliminated players can no longer vote nor be voted for, on both the client
screen and the host listener.
- Nearby listeners are removed on dispose; votes are no longer dispatched
twice; votacionResultado no longer triggers two navigations.
- _jugadoresHostControlados handed the secret word to the host's own
impostors in the payload.
- Impostor cap unified as maxImpostoresPara(n) for both modes, and the
silent clamp in multi-device now reports the effective number.
- palabraAleatoria uses Random.secure.
Impostors know each other
- ConfigPartida.impostoresSeConocen, on by default. companerosImpostores is
nullable: null means nothing to show, an empty list means "you are the
only impostor".
Reconnection
- IdentidadDispositivo persists a stable device id; the host uses it as the
clientId so a phone that drops and returns is the same client. Falls back
to the endpointId for clients that do not send one.
- Usuario.absorbidoDe records the original owner when the host takes over a
disconnected player, and they are handed back automatically on return.
- New solicitarResync/resync messages: the host replies with the current
phase plus that client's own players, and the client jumps straight to the
running phase.
- Readiness is keyed by clientId instead of endpointId, and a disconnected
client no longer blocks the round.
- The retry gives up after three minutes, only reconnects to the host it
belonged to, and is cancelled correctly while shutting down.
Per-word clue support
- The word bank loader accepts both the old list-of-strings format and the
new one carrying a clue per word, falling back to the category clue.
Not verified on real devices: Nearby reconnection timing still needs two
Android phones.
1336 lines
40 KiB
Dart
1336 lines
40 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
import '../l10n/generated/app_localizations.dart';
|
|
import '../modelos/gamificacion_usuario.dart';
|
|
import 'tema_app.dart';
|
|
|
|
class IconoFarolero extends StatelessWidget {
|
|
final IconData icono;
|
|
final String? assetPath;
|
|
final Color? color;
|
|
final double size;
|
|
|
|
const IconoFarolero(
|
|
this.icono, {
|
|
super.key,
|
|
this.assetPath,
|
|
this.color,
|
|
this.size = 28,
|
|
});
|
|
|
|
static String? assetFor(IconData icono) {
|
|
if (icono == Icons.arrow_back) return 'assets/ui/generated/icons/nav_back.webp';
|
|
if (icono == Icons.close) return 'assets/ui/generated/icons/nav_close.webp';
|
|
if (icono == Icons.check ||
|
|
icono == Icons.check_circle ||
|
|
icono == Icons.check_circle_outline) {
|
|
return 'assets/ui/generated/icons/status_check.webp';
|
|
}
|
|
if (icono == Icons.arrow_forward ||
|
|
icono == Icons.arrow_forward_ios ||
|
|
icono == Icons.chevron_right_rounded ||
|
|
icono == Icons.skip_next) {
|
|
return 'assets/ui/generated/icons/nav_forward.webp';
|
|
}
|
|
if (icono == Icons.home) return 'assets/ui/generated/icons/nav_home.webp';
|
|
if (icono == Icons.replay) return 'assets/ui/generated/icons/action_replay.webp';
|
|
if (icono == Icons.save) return 'assets/ui/generated/icons/action_save.webp';
|
|
if (icono == Icons.settings_rounded) return 'assets/ui/generated/icons/settings_gear.webp';
|
|
if (icono == Icons.delete_outline) return 'assets/ui/generated/icons/action_delete.webp';
|
|
if (icono == Icons.lock) return 'assets/ui/generated/icons/status_lock.webp';
|
|
if (icono == Icons.key) return 'assets/ui/generated/icons/status_key.webp';
|
|
if (icono == Icons.visibility) return 'assets/ui/generated/icons/action_reveal_eye.webp';
|
|
if (icono == Icons.visibility_off) return 'assets/ui/generated/icons/action_hide_eye.webp';
|
|
if (icono == Icons.add ||
|
|
icono == Icons.add_circle_outline ||
|
|
icono == Icons.person_add ||
|
|
icono == Icons.person_add_alt_1) {
|
|
return 'assets/ui/generated/icons/control_plus.webp';
|
|
}
|
|
if (icono == Icons.remove_circle_outline) return 'assets/ui/generated/icons/control_minus.webp';
|
|
if (icono == Icons.cancel || icono == Icons.person_off) {
|
|
return 'assets/ui/generated/icons/status_cancel.webp';
|
|
}
|
|
if (icono == Icons.link_off) return 'assets/ui/generated/icons/status_disconnected.webp';
|
|
if (icono == Icons.alternate_email) return 'assets/ui/generated/icons/profile_identity.webp';
|
|
if (icono == Icons.question_mark_rounded) return 'assets/ui/generated/icons/help_question.webp';
|
|
if (icono == Icons.bolt_rounded) return 'assets/ui/generated/icons/quick_bolt.webp';
|
|
if (icono == Icons.mood) return 'assets/ui/generated/icons/mood_happy.webp';
|
|
if (icono == Icons.sentiment_dissatisfied) return 'assets/ui/generated/icons/mood_sad.webp';
|
|
if (icono == Icons.search ||
|
|
icono == Icons.person_search ||
|
|
icono == Icons.gps_fixed) {
|
|
return 'assets/ui/generated/actions/action_join_search.webp';
|
|
}
|
|
if (icono == Icons.how_to_vote) return 'assets/ui/generated/actions/action_vote_mask.webp';
|
|
if (icono == Icons.edit_note || icono == Icons.note) {
|
|
return 'assets/ui/generated/actions/action_notes_quill.webp';
|
|
}
|
|
if (icono == Icons.theater_comedy ||
|
|
icono == Icons.theater_comedy_rounded ||
|
|
icono == Icons.psychology) {
|
|
return 'assets/ui/generated/icons/impostor_shadow.webp';
|
|
}
|
|
if (icono == Icons.emoji_events ||
|
|
icono == Icons.celebration ||
|
|
icono == Icons.bar_chart) {
|
|
return icono == Icons.bar_chart
|
|
? 'assets/ui/generated/icons/stats_chart.webp'
|
|
: 'assets/ui/generated/icons/winner_laurel.webp';
|
|
}
|
|
if (icono == Icons.forum ||
|
|
icono == Icons.chat_bubble ||
|
|
icono == Icons.record_voice_over) {
|
|
return 'assets/ui/generated/actions/action_rules_book.webp';
|
|
}
|
|
if (icono == Icons.person) return 'assets/ui/generated/icons/profile_player.webp';
|
|
if (icono == Icons.groups ||
|
|
icono == Icons.groups_rounded) {
|
|
return 'assets/ui/generated/actions/action_players_group.webp';
|
|
}
|
|
if (icono == Icons.devices ||
|
|
icono == Icons.devices_rounded ||
|
|
icono == Icons.phone_android ||
|
|
icono == Icons.phone_android_rounded) {
|
|
return 'assets/ui/generated/actions/action_mobile_device.webp';
|
|
}
|
|
if (icono == Icons.wifi_tethering ||
|
|
icono == Icons.bluetooth_searching ||
|
|
icono == Icons.radar ||
|
|
icono == Icons.sync) {
|
|
return 'assets/ui/generated/actions/action_multidevice_signal.webp';
|
|
}
|
|
if (icono == Icons.qr_code_scanner) return 'assets/ui/generated/actions/action_qr_scan.webp';
|
|
if (icono == Icons.history_rounded) return 'assets/ui/generated/actions/action_history_ledger.webp';
|
|
if (icono == Icons.menu_book_rounded || icono == Icons.lightbulb) {
|
|
return 'assets/ui/generated/actions/action_rules_book.webp';
|
|
}
|
|
if (icono == Icons.hourglass_bottom || icono == Icons.hourglass_empty) {
|
|
return 'assets/ui/generated/icons/timer_hourglass.webp';
|
|
}
|
|
if (icono == Icons.play_arrow || icono == Icons.play_arrow_rounded) {
|
|
return 'assets/ui/generated/actions/action_create_game.webp';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final resolved = assetPath ?? assetFor(icono);
|
|
if (resolved == null) {
|
|
return Icon(icono, color: color, size: size);
|
|
}
|
|
return Image.asset(
|
|
resolved,
|
|
width: size,
|
|
height: size,
|
|
fit: BoxFit.contain,
|
|
filterQuality: FilterQuality.medium,
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
Icon(icono, color: color, size: size),
|
|
);
|
|
}
|
|
}
|
|
|
|
class FondoFarolero extends StatelessWidget {
|
|
final Widget child;
|
|
final bool intenso;
|
|
|
|
const FondoFarolero({
|
|
super.key,
|
|
required this.child,
|
|
this.intenso = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return DecoratedBox(
|
|
decoration: const BoxDecoration(gradient: TemaApp.gradienteFondo),
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: IgnorePointer(
|
|
child: Image.asset(
|
|
'assets/ui/generated/shared/screen_atmosphere_bg.webp',
|
|
fit: BoxFit.cover,
|
|
opacity: AlwaysStoppedAnimation(intenso ? 0.76 : 0.48),
|
|
filterQuality: FilterQuality.high,
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
const SizedBox.shrink(),
|
|
),
|
|
),
|
|
),
|
|
Positioned.fill(child: child),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PanelFarolero extends StatelessWidget {
|
|
final Widget child;
|
|
final EdgeInsetsGeometry padding;
|
|
final EdgeInsetsGeometry? margin;
|
|
final Color? color;
|
|
final Color? borderColor;
|
|
|
|
const PanelFarolero({
|
|
super.key,
|
|
required this.child,
|
|
this.padding = const EdgeInsets.all(16),
|
|
this.margin,
|
|
this.color,
|
|
this.borderColor,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: double.infinity,
|
|
margin: margin,
|
|
decoration: TemaApp.decoracionPanel(color: color, borderColor: borderColor),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(14),
|
|
child: Stack(
|
|
children: [
|
|
Padding(padding: padding, child: child),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class EncabezadoFarolero extends StatelessWidget {
|
|
final IconData icono;
|
|
final String? assetIconPath;
|
|
final String titulo;
|
|
final String? subtitulo;
|
|
final Color color;
|
|
final Widget? trailing;
|
|
final EdgeInsetsGeometry padding;
|
|
|
|
const EncabezadoFarolero({
|
|
super.key,
|
|
required this.icono,
|
|
this.assetIconPath,
|
|
required this.titulo,
|
|
this.subtitulo,
|
|
this.color = TemaApp.colorNaranja,
|
|
this.trailing,
|
|
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PanelFarolero(
|
|
padding: padding,
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 52,
|
|
height: 52,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [
|
|
color.withValues(alpha: 0.34),
|
|
TemaApp.colorSuperficie.withValues(alpha: 0.72),
|
|
],
|
|
),
|
|
border: Border.all(color: color.withValues(alpha: 0.72)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: color.withValues(alpha: 0.22),
|
|
blurRadius: 22,
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(6),
|
|
child: IconoFarolero(
|
|
icono,
|
|
assetPath: assetIconPath,
|
|
color: color,
|
|
size: 30,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
titulo,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
if (subtitulo != null) ...[
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
subtitulo!,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
if (trailing != null) ...[
|
|
const SizedBox(width: 12),
|
|
trailing!,
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class EstadoVacioFarolero extends StatelessWidget {
|
|
final IconData icono;
|
|
final String? assetIconPath;
|
|
final String titulo;
|
|
final String subtitulo;
|
|
|
|
const EstadoVacioFarolero({
|
|
super.key,
|
|
required this.icono,
|
|
this.assetIconPath,
|
|
required this.titulo,
|
|
required this.subtitulo,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PanelFarolero(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 28),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
SizedBox(
|
|
width: 64,
|
|
height: 64,
|
|
child: IconoFarolero(
|
|
icono,
|
|
assetPath: assetIconPath,
|
|
color: TemaApp.colorNaranja,
|
|
size: 58,
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
Text(
|
|
titulo,
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
subtitulo,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class LogoFarolero extends StatelessWidget {
|
|
final double size;
|
|
|
|
const LogoFarolero({super.key, this.size = 64});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Positioned(
|
|
top: 0,
|
|
child: IconoFarolero(
|
|
Icons.lightbulb,
|
|
color: TemaApp.colorDorado.withValues(alpha: 0.32),
|
|
size: size * 0.82,
|
|
),
|
|
),
|
|
Text(
|
|
'FAROLERO',
|
|
style: GoogleFonts.bangers(
|
|
fontSize: size,
|
|
color: TemaApp.colorNaranja,
|
|
letterSpacing: 0,
|
|
shadows: const [
|
|
Shadow(offset: Offset(3, 4), blurRadius: 0, color: Color(0xFF5E1205)),
|
|
Shadow(offset: Offset(0, 0), blurRadius: 16, color: Color(0xFFFFC247)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class BotonFarolero extends StatelessWidget {
|
|
final String texto;
|
|
final IconData icono;
|
|
final String? assetIconPath;
|
|
final VoidCallback? onPressed;
|
|
final LinearGradient gradient;
|
|
final Color foreground;
|
|
final bool destacado;
|
|
|
|
const BotonFarolero({
|
|
super.key,
|
|
required this.texto,
|
|
required this.icono,
|
|
this.assetIconPath,
|
|
required this.onPressed,
|
|
this.gradient = TemaApp.gradientePrimario,
|
|
this.foreground = Colors.black,
|
|
this.destacado = true,
|
|
});
|
|
|
|
const BotonFarolero.secundario({
|
|
super.key,
|
|
required this.texto,
|
|
required this.icono,
|
|
this.assetIconPath,
|
|
required this.onPressed,
|
|
}) : gradient = const LinearGradient(
|
|
colors: [TemaApp.colorPurpura, Color(0xFF2B1736)],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
foreground = Colors.white,
|
|
destacado = false;
|
|
|
|
const BotonFarolero.oscuro({
|
|
super.key,
|
|
required this.texto,
|
|
required this.icono,
|
|
this.assetIconPath,
|
|
required this.onPressed,
|
|
}) : gradient = const LinearGradient(
|
|
colors: [Color(0xFF151F27), Color(0xFF090E13)],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
foreground = TemaApp.colorTexto,
|
|
destacado = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final habilitado = onPressed != null;
|
|
final colorTexto = habilitado ? foreground : TemaApp.colorTextoSecundario;
|
|
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(24),
|
|
onTap: onPressed,
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(minHeight: 66),
|
|
child: CustomPaint(
|
|
painter: MarcoBotonFaroleroPainter(
|
|
gradient: habilitado
|
|
? gradient
|
|
: const LinearGradient(
|
|
colors: [TemaApp.colorTarjeta, TemaApp.colorSuperficie],
|
|
),
|
|
destacado: destacado,
|
|
habilitado: habilitado,
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 15),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
SizedBox(
|
|
width: 44,
|
|
height: 40,
|
|
child: IconoFarolero(
|
|
icono,
|
|
assetPath: assetIconPath,
|
|
color: colorTexto,
|
|
size: 38,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Center(
|
|
child: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: Text(
|
|
texto.toUpperCase(),
|
|
maxLines: 1,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
color: colorTexto,
|
|
fontSize: 19,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 0.9,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 54),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class TarjetaFaseFarolero extends StatelessWidget {
|
|
final IconData icono;
|
|
final String? assetIconPath;
|
|
final String titulo;
|
|
final String? subtitulo;
|
|
final Color color;
|
|
final Widget child;
|
|
final EdgeInsetsGeometry padding;
|
|
|
|
const TarjetaFaseFarolero({
|
|
super.key,
|
|
required this.icono,
|
|
this.assetIconPath,
|
|
required this.titulo,
|
|
this.subtitulo,
|
|
this.color = TemaApp.colorNaranja,
|
|
this.child = const SizedBox.shrink(),
|
|
this.padding = const EdgeInsets.all(18),
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PanelFarolero(
|
|
padding: padding,
|
|
borderColor: color.withValues(alpha: 0.48),
|
|
color: TemaApp.colorSuperficie.withValues(alpha: 0.84),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 54,
|
|
height: 54,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [
|
|
color.withValues(alpha: 0.42),
|
|
Colors.black.withValues(alpha: 0.62),
|
|
],
|
|
),
|
|
border: Border.all(color: color.withValues(alpha: 0.72)),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(6),
|
|
child: IconoFarolero(
|
|
icono,
|
|
assetPath: assetIconPath,
|
|
color: color,
|
|
size: 30,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
titulo,
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
if (subtitulo != null) ...[
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
subtitulo!,
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: TemaApp.colorTextoSecundario,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
child,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class EstadoJugadorFarolero extends StatelessWidget {
|
|
final String nombre;
|
|
final bool destacado;
|
|
final bool completado;
|
|
final IconData icono;
|
|
final String? assetIconPath;
|
|
final String? subtitulo;
|
|
final VoidCallback? onTap;
|
|
|
|
const EstadoJugadorFarolero({
|
|
super.key,
|
|
required this.nombre,
|
|
this.destacado = false,
|
|
this.completado = false,
|
|
this.icono = Icons.person,
|
|
this.assetIconPath,
|
|
this.subtitulo,
|
|
this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final color = completado ? TemaApp.colorVerde : TemaApp.colorNaranja;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(18),
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: completado
|
|
? TemaApp.colorVerde.withValues(alpha: 0.14)
|
|
: TemaApp.colorTarjeta.withValues(alpha: 0.84),
|
|
borderRadius: BorderRadius.circular(18),
|
|
border: Border.all(
|
|
color: destacado
|
|
? TemaApp.colorDorado.withValues(alpha: 0.72)
|
|
: color.withValues(alpha: completado ? 0.54 : 0.28),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 42,
|
|
height: 42,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: color.withValues(alpha: 0.16),
|
|
border: Border.all(color: color.withValues(alpha: 0.72)),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(5),
|
|
child: IconoFarolero(
|
|
icono,
|
|
assetPath: assetIconPath,
|
|
color: color,
|
|
size: 26,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
nombre,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: destacado ? FontWeight.w900 : FontWeight.w700,
|
|
),
|
|
),
|
|
if (subtitulo != null) ...[
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
subtitulo!,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: TemaApp.colorTextoSecundario,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
IconoFarolero(
|
|
completado ? Icons.check_circle : Icons.hourglass_bottom,
|
|
color: completado ? TemaApp.colorVerde : TemaApp.colorTextoSecundario,
|
|
size: 28,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class SelectorVotoFarolero extends StatelessWidget {
|
|
final String nombre;
|
|
final bool seleccionado;
|
|
final VoidCallback onTap;
|
|
|
|
const SelectorVotoFarolero({
|
|
super.key,
|
|
required this.nombre,
|
|
required this.seleccionado,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return EstadoJugadorFarolero(
|
|
nombre: nombre,
|
|
destacado: seleccionado,
|
|
completado: seleccionado,
|
|
icono: seleccionado ? Icons.check : Icons.person_search,
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
}
|
|
|
|
class TemporizadorFarolero extends StatelessWidget {
|
|
final String etiqueta;
|
|
final String tiempo;
|
|
final bool agotado;
|
|
|
|
const TemporizadorFarolero({
|
|
super.key,
|
|
required this.etiqueta,
|
|
required this.tiempo,
|
|
this.agotado = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final color = agotado ? TemaApp.colorAcento : TemaApp.colorNaranja;
|
|
return PanelFarolero(
|
|
padding: const EdgeInsets.all(22),
|
|
borderColor: color.withValues(alpha: 0.62),
|
|
color: color.withValues(alpha: agotado ? 0.18 : 0.10),
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Image.asset(
|
|
'assets/ui/generated/actions/action_timer_hourglass.webp',
|
|
height: 132,
|
|
fit: BoxFit.contain,
|
|
opacity: const AlwaysStoppedAnimation(0.34),
|
|
),
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
etiqueta,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
tiempo,
|
|
style: Theme.of(context).textTheme.displaySmall?.copyWith(
|
|
color: color,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MarcoBotonFaroleroPainter extends CustomPainter {
|
|
final LinearGradient gradient;
|
|
final bool destacado;
|
|
final bool habilitado;
|
|
final double radio;
|
|
|
|
const MarcoBotonFaroleroPainter({
|
|
required this.gradient,
|
|
required this.destacado,
|
|
required this.habilitado,
|
|
this.radio = 24,
|
|
});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final rect = Offset.zero & size;
|
|
final cuerpo = rect.deflate(3);
|
|
final rrect = RRect.fromRectAndRadius(cuerpo, Radius.circular(radio));
|
|
final alpha = habilitado ? 1.0 : 0.42;
|
|
final oro = TemaApp.colorDorado.withValues(alpha: 0.95 * alpha);
|
|
final brillo = TemaApp.colorNaranja.withValues(alpha: 0.80 * alpha);
|
|
final sombra = Paint()
|
|
..color = Colors.black.withValues(alpha: 0.38)
|
|
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12);
|
|
|
|
canvas.drawRRect(rrect.shift(const Offset(0, 8)), sombra);
|
|
|
|
final base = Paint()..shader = gradient.createShader(cuerpo);
|
|
canvas.drawRRect(rrect, base);
|
|
|
|
final vignette = Paint()
|
|
..shader = LinearGradient(
|
|
colors: [
|
|
Colors.white.withValues(alpha: destacado ? 0.34 : 0.08),
|
|
Colors.transparent,
|
|
Colors.black.withValues(alpha: destacado ? 0.10 : 0.34),
|
|
],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
).createShader(cuerpo);
|
|
canvas.drawRRect(rrect, vignette);
|
|
|
|
final bordeExterior = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = destacado ? 2.4 : 1.8
|
|
..shader = LinearGradient(
|
|
colors: [oro, Colors.white.withValues(alpha: 0.72 * alpha), brillo, oro],
|
|
).createShader(cuerpo);
|
|
canvas.drawRRect(rrect, bordeExterior);
|
|
|
|
final bordeInterior = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 1.1
|
|
..color = Colors.white.withValues(alpha: destacado ? 0.38 * alpha : 0.16 * alpha);
|
|
canvas.drawRRect(
|
|
RRect.fromRectAndRadius(cuerpo.deflate(6), Radius.circular(math.max(8, radio - 7))),
|
|
bordeInterior,
|
|
);
|
|
|
|
final railPaint = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 1.5
|
|
..color = oro.withValues(alpha: destacado ? 0.48 * alpha : 0.34 * alpha);
|
|
final yTop = cuerpo.top + size.height * 0.25;
|
|
final yBottom = cuerpo.bottom - size.height * 0.25;
|
|
canvas.drawLine(Offset(cuerpo.left + 54, yTop), Offset(cuerpo.right - 54, yTop), railPaint);
|
|
canvas.drawLine(
|
|
Offset(cuerpo.left + 54, yBottom),
|
|
Offset(cuerpo.right - 54, yBottom),
|
|
railPaint,
|
|
);
|
|
|
|
_drawSideOrnament(canvas, cuerpo, true, oro, brillo, alpha);
|
|
_drawSideOrnament(canvas, cuerpo, false, oro, brillo, alpha);
|
|
_drawCenterJewel(canvas, cuerpo.topCenter, 1, oro, brillo, alpha);
|
|
_drawCenterJewel(canvas, cuerpo.bottomCenter, -1, oro, brillo, alpha);
|
|
}
|
|
|
|
void _drawSideOrnament(
|
|
Canvas canvas,
|
|
Rect rect,
|
|
bool left,
|
|
Color oro,
|
|
Color brillo,
|
|
double alpha,
|
|
) {
|
|
final cx = left ? rect.left + 24 : rect.right - 24;
|
|
final cy = rect.center.dy;
|
|
final dir = left ? -1.0 : 1.0;
|
|
final path = Path()
|
|
..moveTo(cx, cy - 18)
|
|
..lineTo(cx + dir * 24, cy)
|
|
..lineTo(cx, cy + 18)
|
|
..lineTo(cx - dir * 7, cy)
|
|
..close();
|
|
final paint = Paint()
|
|
..shader = LinearGradient(
|
|
colors: [oro, brillo, Colors.white.withValues(alpha: 0.70 * alpha)],
|
|
).createShader(path.getBounds());
|
|
canvas.drawPath(path, paint);
|
|
canvas.drawPath(
|
|
path,
|
|
Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 1
|
|
..color = Colors.white.withValues(alpha: 0.42 * alpha),
|
|
);
|
|
}
|
|
|
|
void _drawCenterJewel(
|
|
Canvas canvas,
|
|
Offset anchor,
|
|
double direction,
|
|
Color oro,
|
|
Color brillo,
|
|
double alpha,
|
|
) {
|
|
final cy = anchor.dy + direction * 3;
|
|
final path = Path()
|
|
..moveTo(anchor.dx, cy - direction * 9)
|
|
..lineTo(anchor.dx + 12, cy)
|
|
..lineTo(anchor.dx, cy + direction * 9)
|
|
..lineTo(anchor.dx - 12, cy)
|
|
..close();
|
|
canvas.drawPath(
|
|
path,
|
|
Paint()
|
|
..shader = LinearGradient(
|
|
colors: [Colors.white.withValues(alpha: 0.78 * alpha), oro, brillo],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
).createShader(path.getBounds()),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant MarcoBotonFaroleroPainter oldDelegate) {
|
|
return oldDelegate.gradient != gradient ||
|
|
oldDelegate.destacado != destacado ||
|
|
oldDelegate.habilitado != habilitado ||
|
|
oldDelegate.radio != radio;
|
|
}
|
|
}
|
|
|
|
class ArteGameplayFarolero extends StatelessWidget {
|
|
final String assetPath;
|
|
final double height;
|
|
final double opacity;
|
|
final EdgeInsetsGeometry padding;
|
|
|
|
const ArteGameplayFarolero({
|
|
super.key,
|
|
required this.assetPath,
|
|
this.height = 128,
|
|
this.opacity = 0.92,
|
|
this.padding = EdgeInsets.zero,
|
|
});
|
|
|
|
const ArteGameplayFarolero.fase({
|
|
super.key,
|
|
this.height = 128,
|
|
this.opacity = 0.92,
|
|
this.padding = EdgeInsets.zero,
|
|
}) : assetPath = 'assets/ui/generated/gameplay/gameplay_phase_emblem.webp';
|
|
|
|
const ArteGameplayFarolero.notas({
|
|
super.key,
|
|
this.height = 150,
|
|
this.opacity = 0.94,
|
|
this.padding = EdgeInsets.zero,
|
|
}) : assetPath = 'assets/ui/generated/gameplay/notes_strategy_art.webp';
|
|
|
|
const ArteGameplayFarolero.ajustes({
|
|
super.key,
|
|
this.height = 150,
|
|
this.opacity = 0.94,
|
|
this.padding = EdgeInsets.zero,
|
|
}) : assetPath = 'assets/ui/generated/meta/settings_profile_art.webp';
|
|
|
|
const ArteGameplayFarolero.historial({
|
|
super.key,
|
|
this.height = 150,
|
|
this.opacity = 0.94,
|
|
this.padding = EdgeInsets.zero,
|
|
}) : assetPath = 'assets/ui/generated/meta/history_ledger_art.webp';
|
|
|
|
const ArteGameplayFarolero.resultado({
|
|
super.key,
|
|
this.height = 150,
|
|
this.opacity = 0.94,
|
|
this.padding = EdgeInsets.zero,
|
|
}) : assetPath = 'assets/ui/generated/meta/result_verdict_art.webp';
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ExcludeSemantics(
|
|
child: Padding(
|
|
padding: padding,
|
|
child: SizedBox(
|
|
height: height,
|
|
width: double.infinity,
|
|
child: Image.asset(
|
|
assetPath,
|
|
fit: BoxFit.contain,
|
|
opacity: AlwaysStoppedAnimation(opacity),
|
|
filterQuality: FilterQuality.high,
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
const SizedBox.shrink(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AccesoFarolero extends StatelessWidget {
|
|
final String etiqueta;
|
|
final IconData icono;
|
|
final VoidCallback onPressed;
|
|
|
|
const AccesoFarolero({
|
|
super.key,
|
|
required this.etiqueta,
|
|
required this.icono,
|
|
required this.onPressed,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(8),
|
|
onTap: onPressed,
|
|
child: Ink(
|
|
height: 66,
|
|
decoration: TemaApp.decoracionPanel(color: TemaApp.colorSuperficie),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
IconoFarolero(icono, color: TemaApp.colorNaranja, size: 24),
|
|
const SizedBox(height: 5),
|
|
Text(
|
|
etiqueta.toUpperCase(),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class TarjetaPalabraFarolero extends StatelessWidget {
|
|
final String palabra;
|
|
|
|
const TarjetaPalabraFarolero({super.key, required this.palabra});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 28),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFC48642),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: const Color(0xFF6B3519), width: 2),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.28),
|
|
blurRadius: 24,
|
|
),
|
|
],
|
|
),
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Positioned.fill(
|
|
child: Image.asset(
|
|
'assets/ui/generated/gameplay/gameplay_phase_emblem.webp',
|
|
fit: BoxFit.contain,
|
|
opacity: const AlwaysStoppedAnimation(0.14),
|
|
filterQuality: FilterQuality.high,
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
const SizedBox.shrink(),
|
|
),
|
|
),
|
|
Text(
|
|
palabra.toUpperCase(),
|
|
textAlign: TextAlign.center,
|
|
style: GoogleFonts.oswald(
|
|
color: const Color(0xFF1B0C05),
|
|
fontSize: 42,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 0,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Pista que ve el impostor cuando la partida la tiene activada.
|
|
class PistaImpostorFarolero extends StatelessWidget {
|
|
final String pista;
|
|
|
|
const PistaImpostorFarolero({super.key, required this.pista});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Text(
|
|
AppLocalizations.of(context)!.clueIs(pista),
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.bodyLarge?.copyWith(color: TemaApp.colorNaranja),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Panel con el resto de impostores. Solo se muestra a un impostor y solo
|
|
/// cuando la partida permite que se conozcan entre ellos.
|
|
class CompanerosImpostorFarolero extends StatelessWidget {
|
|
final List<String> nombres;
|
|
|
|
const CompanerosImpostorFarolero({super.key, required this.nombres});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
if (nombres.isEmpty) {
|
|
return Text(
|
|
l10n.youAreTheOnlyImpostor,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.bodyMedium?.copyWith(color: TemaApp.colorTextoSecundario),
|
|
);
|
|
}
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
decoration: TemaApp.decoracionPanel(
|
|
color: TemaApp.colorAcento.withValues(alpha: 0.16),
|
|
borderColor: TemaApp.colorAcento.withValues(alpha: 0.65),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
IconoFarolero(
|
|
Icons.groups,
|
|
color: TemaApp.colorAcento,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Flexible(
|
|
child: Text(
|
|
l10n.otherImpostorsTitle(nombres.length),
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.titleSmall,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
nombres.join(' · '),
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
color: TemaApp.colorAcento,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AvatarFarolero extends StatelessWidget {
|
|
final String texto;
|
|
final String? assetPath;
|
|
final Color color;
|
|
final double size;
|
|
final int fuego;
|
|
final List<String> medallas;
|
|
|
|
const AvatarFarolero({
|
|
super.key,
|
|
required this.texto,
|
|
this.assetPath,
|
|
this.color = TemaApp.colorNaranja,
|
|
this.size = 40,
|
|
this.fuego = 0,
|
|
this.medallas = const [],
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final lienzo = size + 10;
|
|
return SizedBox(
|
|
width: lienzo,
|
|
height: lienzo,
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
alignment: Alignment.center,
|
|
children: [
|
|
CustomPaint(
|
|
size: Size(lienzo, lienzo),
|
|
painter: _FuegoAvatarPainter(fuego: fuego),
|
|
),
|
|
Container(
|
|
width: size,
|
|
height: size,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [color.withValues(alpha: 0.9), TemaApp.colorSuperficie],
|
|
),
|
|
border: Border.all(color: TemaApp.colorDorado, width: 2),
|
|
),
|
|
child: Center(
|
|
child: assetPath == null
|
|
? Text(
|
|
texto,
|
|
style: TextStyle(
|
|
color: TemaApp.colorTexto,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: size * 0.36,
|
|
),
|
|
)
|
|
: ClipOval(
|
|
child: Image.asset(
|
|
assetPath!,
|
|
width: size,
|
|
height: size,
|
|
fit: BoxFit.contain,
|
|
filterQuality: FilterQuality.high,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (medallas.isNotEmpty)
|
|
Positioned(
|
|
right: -2,
|
|
bottom: -4,
|
|
child: _MiniMedalla(id: medallas.first),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MedallasCompactasFarolero extends StatelessWidget {
|
|
final List<String> ids;
|
|
final int max;
|
|
|
|
const MedallasCompactasFarolero({
|
|
super.key,
|
|
required this.ids,
|
|
this.max = 3,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final visibles = ids.take(max).toList();
|
|
if (visibles.isEmpty) return const SizedBox.shrink();
|
|
return Wrap(
|
|
spacing: 4,
|
|
runSpacing: 4,
|
|
children: [
|
|
for (final id in visibles) _MiniMedalla(id: id),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _MiniMedalla extends StatelessWidget {
|
|
final String id;
|
|
|
|
const _MiniMedalla({required this.id});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final medalla = EstadisticasPerfilUsuario.catalogoMedallas[id];
|
|
if (medalla == null) return const SizedBox.shrink();
|
|
return Tooltip(
|
|
message: '${medalla.nombre}: ${medalla.descripcion}',
|
|
child: Container(
|
|
width: 26,
|
|
height: 26,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: TemaApp.colorSuperficie.withValues(alpha: 0.92),
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.32),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Image.asset(
|
|
medalla.assetPath,
|
|
width: 26,
|
|
height: 26,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
Text(medalla.emoji, style: const TextStyle(fontSize: 12)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FuegoAvatarPainter extends CustomPainter {
|
|
final int fuego;
|
|
|
|
const _FuegoAvatarPainter({required this.fuego});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final porcentaje = fuego.clamp(0, 100) / 100;
|
|
final rect = Offset.zero & size;
|
|
final centro = rect.center;
|
|
final radio = math.min(size.width, size.height) / 2 - 3;
|
|
final base = Paint()
|
|
..isAntiAlias = true
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 4
|
|
..strokeCap = StrokeCap.round
|
|
..color = Colors.black.withValues(alpha: 0.28);
|
|
canvas.drawCircle(centro, radio, base);
|
|
if (porcentaje <= 0) return;
|
|
|
|
final fuegoPaint = Paint()
|
|
..isAntiAlias = true
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 4
|
|
..strokeCap = StrokeCap.round
|
|
..shader = const SweepGradient(
|
|
colors: [Color(0xFFFFC247), Color(0xFFFF7A1A), Color(0xFFE53935)],
|
|
).createShader(Rect.fromCircle(center: centro, radius: radio));
|
|
canvas.drawArc(
|
|
Rect.fromCircle(center: centro, radius: radio),
|
|
-math.pi / 2,
|
|
math.pi * 2 * porcentaje,
|
|
false,
|
|
fuegoPaint,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _FuegoAvatarPainter oldDelegate) {
|
|
return oldDelegate.fuego != fuego;
|
|
}
|
|
}
|