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.
295 lines
12 KiB
Dart
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],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|