feat(app): add onboarding and harden alarms
This commit is contained in:
@@ -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 what’s new.',
|
||||
};
|
||||
|
||||
String _formatearDuracionTimer(Duration duracion) {
|
||||
final horas = duracion.inHours;
|
||||
final minutos = duracion.inMinutes.remainder(60);
|
||||
|
||||
Reference in New Issue
Block a user