import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../estado/estado_navegacion.dart'; import '../l10n/gen/app_localizations.dart'; import '../servicios/servicio_bienvenida.dart'; import '../tema/pluriwave_theme.dart'; /// WU17: first-run welcome surface (`onboarding-welcome` spec, mockup /// screen 14) rebuilt WITHOUT its monetization content — no "PRO" pill, no /// "14 días" trial line, no pricing card, and no secondary "free version" /// link (binding no-monetization requirement). Content only: logo, /// headline, body copy, exactly 3 feature bullets, and the single /// "Empezar a escuchar" CTA. /// /// A full-screen ROUTE, not a modal `Dialog` — deliberately distinct from /// the pre-existing `PluriOnboardingDialog` (an unrelated "what's new" /// help-content modal already shown from `app.dart`'s launch flow). Both /// surfaces coexist: [mostrarSiProcede] (WU17b) is what actually wires this /// screen into the genuine first-launch flow, called from `app.dart` BEFORE /// `PluriOnboardingDialog.mostrarSiProcede` on every cold start, so the two /// never race — the once-ever welcome resolves first, then the recurring /// what's-new dialog runs its own unrelated per-version check exactly as /// before. class PantallaBienvenida extends StatelessWidget { const PantallaBienvenida({super.key}); static final ServicioBienvenida _servicio = ServicioBienvenida(); /// WU17b: shows this screen once, on the genuine first launch, then never /// again. Mirrors `PluriOnboardingDialog.mostrarSiProcede`'s shape /// (check-then-show-then-mark-seen) so both first-launch surfaces share /// the same call convention from `app.dart`. static Future mostrarSiProcede(BuildContext context) async { if (!await _servicio.debeMostrarBienvenida()) return; if (!context.mounted) return; await Navigator.of( context, ).push(MaterialPageRoute(builder: (_) => const PantallaBienvenida())); await _servicio.marcarBienvenidaVista(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final t = context.pluriTokens; final theme = Theme.of(context); return Scaffold( body: Stack( fit: StackFit.expand, children: [ const _FondoBienvenida(), SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Audit 14.2 (t4 lines 693/706): the prototype anchors // this content to the BOTTOM with two flexible spacers, // so the CTA sits at the screen edge. The scrollable // Expanded region above the CTA keeps this safe on short // screens / large text scale instead of a rigid Spacer // that could overflow. Expanded( child: SingleChildScrollView( padding: const EdgeInsets.only(top: 48), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Audit 14.8 (t4 line 693): the prototype clips // its logo mark to a 20px rounded rect; the build // used to paint it square. ClipRRect( borderRadius: BorderRadius.circular(20), child: Image.asset( 'assets/icons/pluriwave_app_mark.png', key: const ValueKey('welcome-logo'), width: 76, height: 76, errorBuilder: (_, __, ___) => Icon( Icons.graphic_eq_rounded, size: 76, color: t.electricMagenta, ), ), ), const SizedBox(height: 22), Text( l10n.welcomeHeadline, // Audit 14.3 (t4 line 696): 34px/ls-1.2, not // headlineMedium's 28/ls-1.0 — a local override, // matching the precedent already set by every // other one-off size correction in this screen // (14.4-14.6 below). style: theme.textTheme.headlineMedium?.copyWith( fontSize: 34, fontWeight: FontWeight.w800, height: 1.05, letterSpacing: -1.2, ), ), const SizedBox(height: 12), Text( l10n.welcomeBody, // Audit 14.4 (t4 line 697): 14.5px, not // bodyMedium's 14. style: theme.textTheme.bodyMedium?.copyWith( fontSize: 14.5, color: theme.colorScheme.onSurface.withValues( alpha: 0.72, ), height: 1.5, ), ), const SizedBox(height: 28), FilaCaracteristicaBienvenida( icon: Icons.equalizer_rounded, iconColor: t.electricMagenta, titulo: l10n.welcomeBullet1Title, subtitulo: l10n.welcomeBullet1Subtitle, ), // Audit 14.6 (t4 line 701): 12px between bullets, // not 16. const SizedBox(height: 12), FilaCaracteristicaBienvenida( icon: Icons.directions_car_rounded, iconColor: t.liveGreen, titulo: l10n.welcomeBullet2Title, subtitulo: l10n.welcomeBullet2Subtitle, ), const SizedBox(height: 12), FilaCaracteristicaBienvenida( icon: Icons.alarm_rounded, iconColor: t.warmCoral, titulo: l10n.welcomeBullet3Title, subtitulo: l10n.welcomeBullet3Subtitle, ), ], ), ), ), const SizedBox(height: 24), SizedBox( height: 58, child: FilledButton( onPressed: () => _empezar(context), // Audit 14.7 (t4 line 715): radius 18 — Material 3's // default FilledButton shape is a fully-round // StadiumBorder, which the prototype does not draw. style: FilledButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(18), ), ), child: Text( l10n.welcomeCtaLabel, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w800, ), ), ), ), ], ), ), ), ], ), ); } void _empezar(BuildContext context) { context.read().irA(RaizPluriWave.escuchar); Navigator.of(context).pop(); } } /// One icon-badge + title/subtitle row. Public (not `_FilaCaracteristica`) /// so the structural "exactly 3 feature bullets" guard in /// `pantalla_bienvenida_test.dart` can target it via `find.byType` — the /// same reason WU4 made `FormularioEmisoraPersonalizada` public. class FilaCaracteristicaBienvenida extends StatelessWidget { const FilaCaracteristicaBienvenida({ super.key, required this.icon, required this.iconColor, required this.titulo, required this.subtitulo, }); final IconData icon; final Color iconColor; final String titulo; final String subtitulo; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: iconColor.withValues(alpha: 0.16), borderRadius: BorderRadius.circular(11), ), child: Icon(icon, size: 20, color: iconColor), ), const SizedBox(width: 13), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( titulo, // Audit 14.5 (t4 line 700): 13.5px/w800, not bodyMedium's // 14/w800. style: theme.textTheme.bodyMedium?.copyWith( fontSize: 13.5, fontWeight: FontWeight.w800, ), ), Text( subtitulo, // Audit 14.5 (t4 line 700): 11.5px, not bodySmall's 12. style: theme.textTheme.bodySmall?.copyWith( fontSize: 11.5, color: theme.colorScheme.onSurface.withValues(alpha: 0.58), ), ), ], ), ), ], ); } } /// Full-bleed blurred backdrop (audit 14.1, t4 lines 687-689). Reuses the /// bundled `aurora_wave_banner.png` — the same asset the now-deleted /// `PluriScreenHeader` used (Tier 1, S2) — rather than the prototype's own /// mockup-only `assets/banner.jpg`, which this app does not bundle. Mirrors /// `_FondoArteDifuminado` in `pantalla_alarma_sonando.dart`: blur + a /// deepViolet gradient, no noise layer, purely decorative and not /// spec-tested beyond its own presence. class _FondoBienvenida extends StatelessWidget { const _FondoBienvenida(); @override Widget build(BuildContext context) { final tokens = context.pluriTokens; return Positioned.fill( key: const ValueKey('welcome-background-art'), child: Stack( fit: StackFit.expand, children: [ ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), child: Opacity( opacity: 0.4, child: Image.asset( 'assets/images/aurora_wave_banner.png', fit: BoxFit.cover, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), ), ), DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ tokens.deepViolet.withValues(alpha: 0.45), tokens.deepViolet.withValues(alpha: 0.9), tokens.deepViolet, ], stops: const [0, 0.46, 1], ), ), ), ], ), ); } }