Files
pluriwave/lib/pantallas/pantalla_bienvenida.dart
T
FreeTLab e036f99a61 fix(bienvenida,grabaciones): headline size, CTA radius, storage caption
Screen 14 / audit 14.3+14.7: welcome headline now 34px/ls-1.2 (was
headlineMedium's 28/ls-1.0), CTA restyled to a radius-18 rounded
rectangle instead of Material 3's default StadiumBorder (t4:696,715).

Screen 12 / audit 12.2+12.3: the recordings storage card now shows the
bold "X of Y used" headline ABOVE a 6px/radius-3 bar, followed by a
real folder-path + purge-policy caption below it -- was the bar first
with the "used" string as its only (small, generic) caption (t4:613).
New ARB key recordingsLibraryStorageFolderCaption, translated to all
13 locales.

Item 12.5 (folder/max-size settings living behind the header action
instead of inline on this screen) is a deliberate structural split
from WU15b; the audit itself scopes it out of this pass.
2026-07-30 16:00:30 +02:00

295 lines
12 KiB
Dart

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<void> mostrarSiProcede(BuildContext context) async {
if (!await _servicio.debeMostrarBienvenida()) return;
if (!context.mounted) return;
await Navigator.of(
context,
).push(MaterialPageRoute<void>(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<EstadoNavegacionRaiz>().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],
),
),
),
],
),
);
}
}