feat(app): add onboarding and harden alarms
Build & Deploy Pluriwave / Análisis de código (push) Successful in 21s
Build & Deploy Pluriwave / Build APK + AAB release (push) Failing after 1m6s

This commit is contained in:
2026-05-23 01:22:37 +02:00
parent 27b8fccac9
commit 896349ad5f
44 changed files with 1772 additions and 241 deletions
+124 -47
View File
@@ -18,6 +18,7 @@ import '../widgets/ecualizador_widget.dart';
import '../widgets/pluri_glass_surface.dart';
import '../widgets/pluri_icon.dart';
import '../widgets/pluri_layout.dart';
import '../widgets/pluri_onboarding_dialog.dart';
import '../widgets/pluri_premium_widgets.dart';
class PantallaAjustes extends StatelessWidget {
@@ -225,7 +226,6 @@ class _SeccionGrabaciones extends StatelessWidget {
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onTap: () => _seleccionarRuta(context),
),
),
Wrap(
@@ -353,7 +353,8 @@ class _SeccionTimerSueno extends StatelessWidget {
icon: const Icon(Icons.restore_rounded),
label: Text(l10n.timerSectionRestoreRecommended),
onPressed:
() => context.read<EstadoRadio>().restaurarTimerSuenoPresets(),
() =>
context.read<EstadoRadio>().restaurarTimerSuenoPresets(),
),
),
],
@@ -387,8 +388,7 @@ class _SeccionIdioma extends StatelessWidget {
final l10n = AppLocalizations.of(context);
final estadoIdioma = context.watch<EstadoIdioma>();
final locale = estadoIdioma.localeSeleccionado;
final valorActual =
locale == null ? _codigoSistema : _codigoLocale(locale);
final valorActual = locale == null ? _codigoSistema : _codigoLocale(locale);
return PluriGlassSurface(
child: Column(
@@ -479,7 +479,8 @@ class _FormularioDuracionTimer extends StatefulWidget {
const _FormularioDuracionTimer();
@override
State<_FormularioDuracionTimer> createState() => _FormularioDuracionTimerState();
State<_FormularioDuracionTimer> createState() =>
_FormularioDuracionTimerState();
}
class _FormularioDuracionTimerState extends State<_FormularioDuracionTimer> {
@@ -506,9 +507,9 @@ class _FormularioDuracionTimerState extends State<_FormularioDuracionTimer> {
seconds: _leer(_segundosCtrl),
);
if (duracion <= Duration.zero) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.durationGreaterThanZero)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.durationGreaterThanZero)));
return;
}
Navigator.pop(context, duracion);
@@ -593,7 +594,9 @@ class _SeccionEcualizador extends StatelessWidget {
const Spacer(),
Chip(
label: Text(
estado.ecualizadorActivo ? l10n.equalizerActive : l10n.equalizerDisabled,
estado.ecualizadorActivo
? l10n.equalizerActive
: l10n.equalizerDisabled,
),
visualDensity: VisualDensity.compact,
),
@@ -701,7 +704,10 @@ class _SeccionOrdenListas extends StatelessWidget {
class _SeccionGruposFavoritos extends StatelessWidget {
const _SeccionGruposFavoritos();
Future<void> _editarGrupo(BuildContext context, [GrupoFavoritos? grupo]) async {
Future<void> _editarGrupo(
BuildContext context, [
GrupoFavoritos? grupo,
]) async {
final l10n = AppLocalizations.of(context);
final controller = TextEditingController(text: grupo?.nombre ?? '');
final nombre = await showModalBottomSheet<String>(
@@ -717,7 +723,9 @@ class _SeccionGruposFavoritos extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
grupo == null ? l10n.favoriteGroupsAdd : l10n.favoriteGroupsEdit,
grupo == null
? l10n.favoriteGroupsAdd
: l10n.favoriteGroupsEdit,
style: Theme.of(ctx).textTheme.titleLarge,
),
const SizedBox(height: 16),
@@ -756,17 +764,26 @@ class _SeccionGruposFavoritos extends StatelessWidget {
}
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(grupo == null ? l10n.favoriteGroupsCreated : l10n.favoriteGroupsUpdated)),
SnackBar(
content: Text(
grupo == null
? l10n.favoriteGroupsCreated
: l10n.favoriteGroupsUpdated,
),
),
);
}
Future<void> _eliminarGrupo(BuildContext context, GrupoFavoritos grupo) async {
Future<void> _eliminarGrupo(
BuildContext context,
GrupoFavoritos grupo,
) async {
final l10n = AppLocalizations.of(context);
await context.read<EstadoRadio>().eliminarGrupoFavoritos(grupo.id);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.favoriteGroupsDeleted)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.favoriteGroupsDeleted)));
}
String _nombreVisible(AppLocalizations l10n, GrupoFavoritos grupo) =>
@@ -808,24 +825,28 @@ class _SeccionGruposFavoritos extends StatelessWidget {
grupo.esSinAsignar ? Icons.lock_rounded : Icons.folder_rounded,
),
title: Text(_nombreVisible(l10n, grupo)),
subtitle: grupo.esSinAsignar ? Text(l10n.favoriteGroupsProtectedHint) : null,
trailing: grupo.esSinAsignar
? null
: Wrap(
spacing: 4,
children: [
IconButton(
tooltip: l10n.favoriteGroupsEdit,
icon: const Icon(Icons.edit_rounded),
onPressed: () => _editarGrupo(context, grupo),
),
IconButton(
tooltip: l10n.favoriteGroupsDelete,
icon: const Icon(Icons.delete_outline_rounded),
onPressed: () => _eliminarGrupo(context, grupo),
),
],
),
subtitle:
grupo.esSinAsignar
? Text(l10n.favoriteGroupsProtectedHint)
: null,
trailing:
grupo.esSinAsignar
? null
: Wrap(
spacing: 4,
children: [
IconButton(
tooltip: l10n.favoriteGroupsEdit,
icon: const Icon(Icons.edit_rounded),
onPressed: () => _editarGrupo(context, grupo),
),
IconButton(
tooltip: l10n.favoriteGroupsDelete,
icon: const Icon(Icons.delete_outline_rounded),
onPressed: () => _eliminarGrupo(context, grupo),
),
],
),
),
],
),
@@ -911,7 +932,10 @@ class _SeccionEmisoraPreferida extends StatelessWidget {
icon: const Icon(Icons.play_arrow_rounded),
label: Text(l10n.preferredStationPlay),
onPressed:
() => context.read<EstadoRadio>().reproducirEmisoraPreferida(),
() =>
context
.read<EstadoRadio>()
.reproducirEmisoraPreferida(),
),
),
],
@@ -925,7 +949,9 @@ class _SeccionEmisoraPreferida extends StatelessWidget {
estado.listaFavoritos.isNotEmpty
? estado.listaFavoritos
: estado.emisorasDisponiblesPreferencia;
final mapa = <String, Emisora>{for (final emisora in base) emisora.uuid: emisora};
final mapa = <String, Emisora>{
for (final emisora in base) emisora.uuid: emisora,
};
if (preferida != null) {
mapa[preferida.uuid] = preferida;
}
@@ -1055,7 +1081,12 @@ class _FormularioEmisoraState extends State<_FormularioEmisora> {
Widget build(BuildContext context) {
final bottom = MediaQuery.of(context).viewInsets.bottom;
return Padding(
padding: EdgeInsets.fromLTRB(PluriLayout.horizontal, PluriLayout.horizontal, PluriLayout.horizontal, PluriLayout.horizontal + bottom),
padding: EdgeInsets.fromLTRB(
PluriLayout.horizontal,
PluriLayout.horizontal,
PluriLayout.horizontal,
PluriLayout.horizontal + bottom,
),
child: Form(
key: _formKey,
child: Column(
@@ -1089,7 +1120,9 @@ class _FormularioEmisoraState extends State<_FormularioEmisora> {
),
keyboardType: TextInputType.url,
validator: (v) {
if (v == null || v.trim().isEmpty) return AppLocalizations.of(context).requiredField;
if (v == null || v.trim().isEmpty) {
return AppLocalizations.of(context).requiredField;
}
final uri = Uri.tryParse(v.trim());
if (uri == null || !uri.hasScheme) return 'URL no válida';
return null;
@@ -1143,9 +1176,13 @@ class _SeccionBackup extends StatelessWidget {
);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).backupExportError(e.toString()))));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).backupExportError(e.toString()),
),
),
);
}
}
}
@@ -1197,9 +1234,13 @@ class _SeccionBackup extends StatelessWidget {
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).backupImportError(e.toString()))));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).backupImportError(e.toString()),
),
),
);
}
}
}
@@ -1264,7 +1305,9 @@ class _SeccionInfo extends StatelessWidget {
variant: PluriIconVariant.filled,
),
title: const Text('PluriWave'),
subtitle: Text(AppLocalizations.of(ctx).appVersionSubtitle(version)),
subtitle: Text(
AppLocalizations.of(ctx).appVersionSubtitle(version),
),
);
},
),
@@ -1274,18 +1317,30 @@ class _SeccionInfo extends StatelessWidget {
(ctx, snap) => ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.favorite_outline),
title: Text(AppLocalizations.of(ctx).savedFavoritesTitle),
title: Text(
AppLocalizations.of(ctx).savedFavoritesTitle,
),
trailing: Text(
snap.data?.toString() ?? '',
style: Theme.of(ctx).textTheme.bodyLarge,
),
),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.help_outline_rounded),
title: Text(_helpTitle(ctx)),
subtitle: Text(_helpSubtitle(ctx)),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () => PluriOnboardingDialog.mostrar(ctx),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.verified_outlined),
title: Text(AppLocalizations.of(ctx).stationFilterTitle),
subtitle: Text(AppLocalizations.of(ctx).stationFilterSubtitle),
subtitle: Text(
AppLocalizations.of(ctx).stationFilterSubtitle,
),
trailing: const Icon(Icons.check_circle, color: Colors.green),
),
const ListTile(
@@ -1302,6 +1357,28 @@ class _SeccionInfo extends StatelessWidget {
}
}
String _helpTitle(BuildContext context) => switch (Localizations.localeOf(
context,
).languageCode) {
'es' => 'Ayuda y tutorial',
'fr' => 'Aide et tutoriel',
'de' => 'Hilfe und Tutorial',
'it' => 'Aiuto e tutorial',
'pt' => 'Ajuda e tutorial',
_ => 'Help and tutorial',
};
String _helpSubtitle(BuildContext context) => switch (Localizations.localeOf(
context,
).languageCode) {
'es' => 'Repasá funciones, consejos y novedades de PluriWave.',
'fr' => 'Revoyez les fonctions, conseils et nouveautés de PluriWave.',
'de' => 'Funktionen, Tipps und Neuigkeiten von PluriWave ansehen.',
'it' => 'Rivedi funzioni, consigli e novità di PluriWave.',
'pt' => 'Revê funções, dicas e novidades do PluriWave.',
_ => 'Review PluriWave features, tips and whats new.',
};
String _formatearDuracionTimer(Duration duracion) {
final horas = duracion.inHours;
final minutos = duracion.inMinutes.remainder(60);