import 'package:flutter/material.dart'; import '../l10n/gen/app_localizations.dart'; import '../servicios/servicio_tutorial_ayuda.dart'; import '../tema/pluriwave_theme.dart'; import '../tema/pluriwave_tokens.dart'; /// 9-screen help/tutorial carousel (mockup screens 5b..5h, reading order /// 1..9). Reachable two ways: /// - [mostrarSiProcede]: the genuine first-launch sequence in `app.dart`, /// run once ever (both fresh installs AND existing installs upgrading to /// this version), via [ServicioTutorialAyuda]'s plain one-time flag. /// - Manually from Ajustes > Info > "Ayuda y tutorial", constructed directly /// with `primerArranque: false`. /// /// [primerArranque] only changes the LAST page's CTA label -- "Empezar a /// escuchar" on a first-launch entry, "Cerrar" otherwise. Every other /// behaviour (Saltar pops immediately, Siguiente advances) is identical /// regardless of entry point; both cases simply pop the route when /// finished, letting whatever screen is already mounted underneath show. class PantallaTutorialAyuda extends StatefulWidget { const PantallaTutorialAyuda({super.key, required this.primerArranque}); final bool primerArranque; static final ServicioTutorialAyuda _servicio = ServicioTutorialAyuda(); static const int cantidadPaginas = 9; /// Shows this carousel once, on the genuine first-launch sequence, then /// never again. Mirrors `PantallaBienvenida.mostrarSiProcede`'s shape /// (check-then-show-then-mark-seen). static Future mostrarSiProcede(BuildContext context) async { if (!await _servicio.debeMostrarTutorial()) return; if (!context.mounted) return; await Navigator.of(context).push( MaterialPageRoute( builder: (_) => const PantallaTutorialAyuda(primerArranque: true), ), ); await _servicio.marcarTutorialVisto(); } @override State createState() => _PantallaTutorialAyudaState(); } class _PantallaTutorialAyudaState extends State { final _controller = PageController(); int _pagina = 0; @override void dispose() { _controller.dispose(); super.dispose(); } bool get _esUltimaPagina => _pagina == PantallaTutorialAyuda.cantidadPaginas - 1; void _saltar() => Navigator.of(context).pop(); void _siguiente() { if (_esUltimaPagina) { Navigator.of(context).pop(); return; } _controller.nextPage( duration: const Duration(milliseconds: 260), curve: Curves.easeOutCubic, ); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final paginas = _construirPaginas(l10n); return Scaffold( body: SafeArea( child: Column( children: [ SizedBox( height: 48, child: Align( alignment: Alignment.centerRight, child: _esUltimaPagina ? null : Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: TextButton( onPressed: _saltar, child: Text(l10n.tutorialSkipAction), ), ), ), ), Expanded( child: PageView.builder( controller: _controller, itemCount: PantallaTutorialAyuda.cantidadPaginas, onPageChanged: (indice) => setState(() => _pagina = indice), itemBuilder: (context, indice) => TarjetaPaginaTutorial(datos: paginas[indice]), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ for ( var i = 0; i < PantallaTutorialAyuda.cantidadPaginas; i++ ) PuntoIndicadorTutorial(activo: i == _pagina), ], ), ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), child: SizedBox( height: 58, width: double.infinity, child: FilledButton( onPressed: _siguiente, style: FilledButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(18), ), ), child: Text( _esUltimaPagina ? (widget.primerArranque ? l10n.welcomeCtaLabel : l10n.closeAction) : l10n.tutorialNextAction, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w800, ), ), ), ), ), ], ), ), ); } List _construirPaginas(AppLocalizations l10n) { final t = PluriWaveTokens.dark; return [ DatosPaginaTutorial( icono: Icons.favorite_rounded, color: t.electricMagenta, titulo: l10n.tutorialPage1Headline, cuerpo: l10n.tutorialPage1Body, ), DatosPaginaTutorial( icono: Icons.equalizer_rounded, color: t.liveGreen, titulo: l10n.tutorialPage2Headline, cuerpo: l10n.tutorialPage2Body, ), DatosPaginaTutorial( icono: Icons.mic_rounded, color: t.warmCoral, titulo: l10n.tutorialPage3Headline, cuerpo: l10n.tutorialPage3Body, ), DatosPaginaTutorial( icono: Icons.alarm_rounded, color: t.offlineAccent, titulo: l10n.tutorialPage4Headline, cuerpo: l10n.tutorialPage4Body, ), DatosPaginaTutorial( icono: Icons.directions_car_rounded, color: PluriWaveTokens.skyBlue, titulo: l10n.tutorialPage5Headline, cuerpo: l10n.tutorialPage5Body, ), DatosPaginaTutorial( icono: Icons.wifi_tethering_rounded, color: t.liveGreen, titulo: l10n.tutorialPage6Headline, cuerpo: l10n.tutorialPage6Body, ), DatosPaginaTutorial( icono: Icons.snooze_rounded, color: t.warmCoral, titulo: l10n.tutorialPage7Headline, cuerpo: l10n.tutorialPage7Body, ), DatosPaginaTutorial( icono: Icons.add_link_rounded, color: PluriWaveTokens.skyBlue, titulo: l10n.tutorialPage8Headline, cuerpo: l10n.tutorialPage8Body, ), DatosPaginaTutorial( icono: Icons.check_circle_rounded, color: t.electricMagenta, titulo: l10n.tutorialPage9Headline, // Last page only: the "watch it again" reminder banner (design // ADR text, mockup screen 5h). No progress-dot advancement beyond // this page -- it is the final one. bannerCuerpo: l10n.tutorialPage9BannerBody, ), ]; } } /// Content for a single carousel page: icon badge, headline, body, and -- /// only on the last page -- the "watch it again" reminder banner. class DatosPaginaTutorial { const DatosPaginaTutorial({ required this.icono, required this.color, required this.titulo, this.cuerpo, this.bannerCuerpo, }); final IconData icono; final Color color; final String titulo; /// Body copy below the headline. `null` on the last page (mockup screen /// 5h), which shows only the headline plus [bannerCuerpo] -- no separate /// body paragraph. final String? cuerpo; final String? bannerCuerpo; } /// One carousel page: a 150x150 rounded-square icon badge, headline, body, /// and -- when [DatosPaginaTutorial.bannerCuerpo] is set -- the reminder /// banner. Public (not `_TarjetaPagina`) so tests can target pages by type, /// same reason `FilaCaracteristicaBienvenida` is public. class TarjetaPaginaTutorial extends StatelessWidget { const TarjetaPaginaTutorial({super.key, required this.datos}); final DatosPaginaTutorial datos; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 150, height: 150, decoration: BoxDecoration( color: datos.color.withValues(alpha: 0.13), borderRadius: BorderRadius.circular(32), ), child: Icon(datos.icono, size: 72, color: datos.color), ), const SizedBox(height: 32), Text( datos.titulo, textAlign: TextAlign.center, style: theme.textTheme.headlineSmall?.copyWith( fontSize: 24, fontWeight: FontWeight.w800, letterSpacing: -0.4, height: 1.15, ), ), if (datos.cuerpo case final cuerpo?) ...[ const SizedBox(height: 12), Text( cuerpo, textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( fontSize: 14, height: 1.5, color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), ), ], if (datos.bannerCuerpo case final bannerCuerpo?) ...[ const SizedBox(height: 24), _BannerRecordatorio(texto: bannerCuerpo), ], ], ), ); } } class _BannerRecordatorio extends StatelessWidget { const _BannerRecordatorio({required this.texto}); final String texto; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.white.withValues(alpha: 0.1)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.info_outline_rounded, size: 20, color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), const SizedBox(width: 10), Expanded( child: Text( texto, style: theme.textTheme.bodySmall?.copyWith( fontSize: 12.5, height: 1.4, color: theme.colorScheme.onSurface.withValues(alpha: 0.72), ), ), ), ], ), ); } } /// One dot in the 9-dot progress indicator: wider and teal when [activo], /// small and translucent otherwise. Public so tests can assert "exactly 9 /// dots" via `find.byType`. class PuntoIndicadorTutorial extends StatelessWidget { const PuntoIndicadorTutorial({super.key, required this.activo}); final bool activo; @override Widget build(BuildContext context) { final t = context.pluriTokens; return AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.symmetric(horizontal: 3), width: activo ? 24 : 8, height: 8, decoration: BoxDecoration( color: activo ? t.electricMagenta : Colors.white.withValues(alpha: 0.24), borderRadius: BorderRadius.circular(4), ), ); } }