import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import '../modelos/gamificacion_usuario.dart'; import 'tema_app.dart'; 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: CustomPaint(painter: _FondoFaroleroPainter(intenso: intenso)), ), 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 titulo; final String? subtitulo; final Color color; final Widget? trailing; final EdgeInsetsGeometry padding; const EncabezadoFarolero({ super.key, required this.icono, 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: Icon(icono, 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 titulo; final String subtitulo; const EstadoVacioFarolero({ super.key, required this.icono, 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: [ Icon(icono, color: TemaApp.colorNaranja, size: 46), 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: Icon( 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 VoidCallback? onPressed; final LinearGradient gradient; final Color foreground; const BotonFarolero({ super.key, required this.texto, required this.icono, required this.onPressed, this.gradient = TemaApp.gradientePrimario, this.foreground = Colors.black, }); const BotonFarolero.secundario({ super.key, required this.texto, required this.icono, required this.onPressed, }) : gradient = const LinearGradient( colors: [TemaApp.colorPurpura, Color(0xFF2B1736)], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), foreground = Colors.white; const BotonFarolero.oscuro({ super.key, required this.texto, required this.icono, required this.onPressed, }) : gradient = const LinearGradient( colors: [Color(0xFF151F27), Color(0xFF090E13)], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), foreground = TemaApp.colorTexto; @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(18), onTap: onPressed, child: Ink( decoration: BoxDecoration( gradient: habilitado ? gradient : const LinearGradient( colors: [TemaApp.colorTarjeta, TemaApp.colorSuperficie], ), borderRadius: BorderRadius.circular(18), border: Border.all( color: habilitado ? TemaApp.colorDorado.withValues(alpha: 0.74) : TemaApp.colorBorde, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.34), blurRadius: 18, offset: const Offset(0, 10), ), if (habilitado) BoxShadow( color: TemaApp.colorNaranja.withValues(alpha: 0.16), blurRadius: 22, ), ], ), child: ConstrainedBox( constraints: const BoxConstraints(minHeight: 64), child: ClipRRect( borderRadius: BorderRadius.circular(18), child: Stack( alignment: Alignment.center, children: [ Positioned.fill( child: IgnorePointer( child: Image.asset( 'assets/ui/generated/main/main_cta_frame.webp', fit: BoxFit.fill, opacity: AlwaysStoppedAnimation(habilitado ? 0.72 : 0.24), filterQuality: FilterQuality.high, errorBuilder: (context, error, stackTrace) => const SizedBox.shrink(), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: 42, child: Icon(icono, color: colorTexto, size: 28), ), const SizedBox(width: 8), 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: 18, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), ), ), ), const SizedBox(width: 50), ], ), ), ], ), ), ), ), ), ); } } 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: [ Icon(icono, color: TemaApp.colorNaranja, size: 22), 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, ), ), ], ), ); } } class AvatarFarolero extends StatelessWidget { final String texto; final String? assetPath; final Color color; final double size; final int fuego; final List 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 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; } } class _FondoFaroleroPainter extends CustomPainter { final bool intenso; const _FondoFaroleroPainter({required this.intenso}); @override void paint(Canvas canvas, Size size) { final paint = Paint()..isAntiAlias = true; final alto = size.height; final ancho = size.width; paint.color = const Color(0xFF152845).withValues(alpha: intenso ? 0.34 : 0.22); canvas.drawCircle(Offset(ancho * 0.78, alto * 0.16), 18, paint); paint.color = const Color(0xFF07101A).withValues(alpha: 0.82); final colinas = Path() ..moveTo(0, alto * 0.34) ..quadraticBezierTo(ancho * 0.28, alto * 0.21, ancho * 0.55, alto * 0.33) ..quadraticBezierTo(ancho * 0.82, alto * 0.43, ancho, alto * 0.26) ..lineTo(ancho, alto) ..lineTo(0, alto) ..close(); canvas.drawPath(colinas, paint); _dibujarCasas(canvas, size, paint); _dibujarFarol(canvas, size, paint); paint.shader = RadialGradient( colors: [ TemaApp.colorNaranja.withValues(alpha: intenso ? 0.26 : 0.16), Colors.transparent, ], ).createShader(Rect.fromCircle(center: Offset(ancho * 0.52, alto * 0.36), radius: 160)); canvas.drawCircle(Offset(ancho * 0.52, alto * 0.36), 160, paint); paint.shader = null; } void _dibujarCasas(Canvas canvas, Size size, Paint paint) { final alto = size.height; final ancho = size.width; paint.color = const Color(0xFF020407).withValues(alpha: 0.72); for (var i = 0; i < 5; i++) { final w = ancho * (0.16 + i * 0.018); final h = alto * (0.18 + (i % 2) * 0.05); final x = -30 + i * ancho * 0.24; final y = alto * (0.72 - i * 0.02); final casa = Rect.fromLTWH(x, y - h, w, h); canvas.drawRect(casa, paint); final tejado = Path() ..moveTo(x - 8, y - h) ..lineTo(x + w * 0.48, y - h - 38) ..lineTo(x + w + 8, y - h) ..close(); canvas.drawPath(tejado, paint); final ventana = Paint() ..color = TemaApp.colorNaranja.withValues(alpha: 0.38) ..isAntiAlias = true; for (var j = 0; j < 2; j++) { canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromLTWH(x + 18 + j * 34, y - h + 36, 12, 22), const Radius.circular(2), ), ventana, ); } } } void _dibujarFarol(Canvas canvas, Size size, Paint paint) { final alto = size.height; final ancho = size.width; final centro = Offset(ancho * 0.5, alto * 0.28); final glow = Paint() ..shader = RadialGradient( colors: [ TemaApp.colorNaranja.withValues(alpha: 0.44), Colors.transparent, ], ).createShader(Rect.fromCircle(center: centro, radius: 92)); canvas.drawCircle(centro, 92, glow); paint ..shader = null ..style = PaintingStyle.stroke ..strokeWidth = 3 ..color = const Color(0xFF050507).withValues(alpha: 0.82); canvas.drawArc( Rect.fromCircle(center: centro.translate(0, -16), radius: 35), math.pi, math.pi, false, paint, ); paint.style = PaintingStyle.fill; canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromCenter(center: centro, width: 38, height: 54), const Radius.circular(5), ), paint, ); paint.color = TemaApp.colorNaranja.withValues(alpha: 0.82); canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromCenter(center: centro, width: 21, height: 34), const Radius.circular(4), ), paint, ); } @override bool shouldRepaint(covariant _FondoFaroleroPainter oldDelegate) { return oldDelegate.intenso != intenso; } }