Files
pluriwave/lib/pantallas/pantalla_ajustes.dart
T
FreeTLab 6480c56f99
Build & Deploy Pluriwave / Análisis de código (push) Successful in 24s
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 1m44s
feat(i18n): migrate settings literals
2026-05-22 13:49:34 +02:00

1002 lines
33 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart' show Share, XFile;
import 'package:uuid/uuid.dart';
import '../estado/estado_idioma.dart';
import '../estado/estado_radio.dart';
import '../l10n/gen/app_localizations.dart';
import '../modelos/emisora.dart';
import '../widgets/ecualizador_widget.dart';
import '../widgets/pluri_glass_surface.dart';
import '../widgets/pluri_icon.dart';
import '../widgets/pluri_layout.dart';
import '../widgets/pluri_premium_widgets.dart';
class PantallaAjustes extends StatelessWidget {
const PantallaAjustes({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return ListView(
padding: PluriLayout.pageListPadding,
children: [
PluriScreenHeader(
title: l10n.settingsTitle,
subtitle: l10n.settingsSubtitle,
glyph: PluriIconGlyph.settings,
trailing: PluriStatusPill(
icon: Icons.security_rounded,
label: l10n.settingsSafeStatus,
),
),
const Padding(
padding: PluriLayout.pageContentPadding,
child: _AjustesContent(),
),
],
);
}
}
class _AjustesContent extends StatelessWidget {
const _AjustesContent();
@override
Widget build(BuildContext context) {
return Column(
children: const [
_SeccionEcualizador(),
SizedBox(height: 12),
_SeccionGrabaciones(),
SizedBox(height: 12),
_SeccionTimerSueno(),
SizedBox(height: 12),
_SeccionIdioma(),
SizedBox(height: 12),
_SeccionEmisoraPreferida(),
SizedBox(height: 12),
_SeccionEmisoras(),
SizedBox(height: 12),
_SeccionBackup(),
SizedBox(height: 12),
_SeccionInfo(),
],
);
}
}
class _SeccionGrabaciones extends StatelessWidget {
const _SeccionGrabaciones();
Future<void> _seleccionarRuta(BuildContext context) async {
final estado = context.read<EstadoRadio>();
final messenger = ScaffoldMessenger.of(context);
final l10n = AppLocalizations.of(context);
final ruta = await FilePicker.platform.getDirectoryPath(
dialogTitle: l10n.recordingsFolderDialogTitle,
);
if (ruta == null) return;
try {
await estado.cambiarDirectorioGrabacion(ruta);
messenger.showSnackBar(
const SnackBar(content: Text('Ruta de grabación actualizada')),
);
} catch (e) {
messenger.showSnackBar(
SnackBar(content: Text(l10n.recordingsPathSaveError(e.toString()))),
);
}
}
Future<void> _restaurarRuta(BuildContext context) async {
final estado = context.read<EstadoRadio>();
final messenger = ScaffoldMessenger.of(context);
await estado.restaurarDirectorioGrabacion();
messenger.showSnackBar(
const SnackBar(content: Text('Se usará la carpeta interna por defecto')),
);
}
@override
Widget build(BuildContext context) {
final estado = context.watch<EstadoRadio>();
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.radio_button_checked),
const SizedBox(width: 12),
Text(
AppLocalizations.of(context).recordingsSectionTitle,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
FutureBuilder<String>(
future: estado.directorioGrabacionEfectivo(),
builder:
(ctx, snap) => ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.folder_outlined),
title: const Text('Carpeta de grabación'),
subtitle: Text(
snap.data ?? AppLocalizations.of(context).recordingsPathCalculating,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onTap: () => _seleccionarRuta(context),
),
),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.folder_open_rounded),
label: Text(AppLocalizations.of(context).recordingsChangePath),
onPressed: () => _seleccionarRuta(context),
),
),
const SizedBox(width: 8),
IconButton.filledTonal(
tooltip: AppLocalizations.of(context).recordingsUseDefaultPath,
icon: const Icon(Icons.restore_rounded),
onPressed: () => _restaurarRuta(context),
),
],
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context).recordingsOriginalStreamHint,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
}
class _SeccionTimerSueno extends StatelessWidget {
const _SeccionTimerSueno();
Future<void> _anadirPreset(BuildContext context) async {
final l10n = AppLocalizations.of(context);
final duracion = await showModalBottomSheet<Duration>(
context: context,
isScrollControlled: true,
showDragHandle: true,
builder: (_) => const _FormularioDuracionTimer(),
);
if (duracion == null || !context.mounted) return;
await context.read<EstadoRadio>().agregarTimerSuenoPreset(duracion);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${l10n.saveQuickAccessButton}: ${_formatearDuracionTimer(duracion)}',
),
),
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final estado = context.watch<EstadoRadio>();
final presets = estado.timerSuenoPresetsSegundos;
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.bedtime_rounded),
const SizedBox(width: 12),
Text(
l10n.timerSectionTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const Spacer(),
TextButton.icon(
icon: const Icon(Icons.add_rounded),
label: Text(l10n.timerSectionAdd),
onPressed: () => _anadirPreset(context),
),
],
),
const SizedBox(height: 8),
Text(
l10n.timerSectionDescription,
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final segundos in presets)
InputChip(
label: Text(
_formatearDuracionTimer(Duration(seconds: segundos)),
),
onDeleted:
presets.length <= 1
? null
: () => context
.read<EstadoRadio>()
.eliminarTimerSuenoPreset(segundos),
),
],
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
icon: const Icon(Icons.restore_rounded),
label: Text(l10n.timerSectionRestoreRecommended),
onPressed:
() => context.read<EstadoRadio>().restaurarTimerSuenoPresets(),
),
),
],
),
);
}
}
class _SeccionIdioma extends StatelessWidget {
const _SeccionIdioma();
static const _codigoSistema = 'system';
static const _codigoEspanol = 'es';
static const _codigoIngles = 'en';
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final estadoIdioma = context.watch<EstadoIdioma>();
final locale = estadoIdioma.localeSeleccionado;
final valorActual = switch (locale?.languageCode) {
_codigoEspanol => _codigoEspanol,
_codigoIngles => _codigoIngles,
_ => _codigoSistema,
};
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.language_rounded),
const SizedBox(width: 12),
Text(
l10n.languageSectionTitle,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 8),
Text(
l10n.languageSectionDescription,
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: valorActual,
decoration: InputDecoration(
labelText: l10n.languageSectionTitle,
border: const OutlineInputBorder(),
),
items: [
DropdownMenuItem(
value: _codigoSistema,
child: Text(l10n.languageSystemDefault),
),
DropdownMenuItem(
value: _codigoEspanol,
child: Text(l10n.languageSpanish),
),
DropdownMenuItem(
value: _codigoIngles,
child: Text(l10n.languageEnglish),
),
],
onChanged: (codigo) async {
if (codigo == null) return;
if (codigo == _codigoSistema) {
await context.read<EstadoIdioma>().seleccionarSistema();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.languageUpdatedSystem)),
);
return;
}
final locale = Locale(codigo);
await context.read<EstadoIdioma>().seleccionarLocale(locale);
if (!context.mounted) return;
final nombre = switch (codigo) {
_codigoEspanol => l10n.languageSpanish,
_codigoIngles => l10n.languageEnglish,
_ => l10n.languageSystemDefault,
};
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.languageUpdated(nombre))),
);
},
),
],
),
);
}
}
class _FormularioDuracionTimer extends StatefulWidget {
const _FormularioDuracionTimer();
@override
State<_FormularioDuracionTimer> createState() => _FormularioDuracionTimerState();
}
class _FormularioDuracionTimerState extends State<_FormularioDuracionTimer> {
final _horasCtrl = TextEditingController();
final _minutosCtrl = TextEditingController(text: '15');
final _segundosCtrl = TextEditingController();
@override
void dispose() {
_horasCtrl.dispose();
_minutosCtrl.dispose();
_segundosCtrl.dispose();
super.dispose();
}
int _leer(TextEditingController ctrl) => int.tryParse(ctrl.text.trim()) ?? 0;
void _guardar() {
final l10n = AppLocalizations.of(context);
final duracion = Duration(
hours: _leer(_horasCtrl),
minutes: _leer(_minutosCtrl),
seconds: _leer(_segundosCtrl),
);
if (duracion <= Duration.zero) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.durationGreaterThanZero)),
);
return;
}
Navigator.pop(context, duracion);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final bottom = MediaQuery.viewInsetsOf(context).bottom;
return SafeArea(
child: Padding(
padding: EdgeInsets.fromLTRB(18, 0, 18, 18 + bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
l10n.newQuickAccessTitle,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(child: _campo(_horasCtrl, l10n.hoursLabel)),
const SizedBox(width: 8),
Expanded(child: _campo(_minutosCtrl, l10n.minutesLabel)),
const SizedBox(width: 8),
Expanded(child: _campo(_segundosCtrl, l10n.secondsLabel)),
],
),
const SizedBox(height: 16),
FilledButton.icon(
icon: const Icon(Icons.save_rounded),
label: Text(l10n.saveQuickAccessButton),
onPressed: _guardar,
),
],
),
),
);
}
Widget _campo(TextEditingController controller, String label) {
return TextField(
controller: controller,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
);
}
}
class _SeccionEcualizador extends StatelessWidget {
const _SeccionEcualizador();
@override
Widget build(BuildContext context) {
return Consumer<EstadoRadio>(
builder: (ctx, estado, _) {
final disponible = estado.ecualizadorDisponible;
final emisoraActual = estado.emisoraActual;
final mostrarModoPorEmisora =
emisoraActual != null && estado.emisoraActualEsFavorita;
final usandoEqPropio = estado.emisoraActualTienePresetPropio;
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Icon(Icons.equalizer),
const SizedBox(width: 12),
Text(
AppLocalizations.of(ctx).equalizerTitle,
style: Theme.of(ctx).textTheme.titleMedium,
),
const Spacer(),
Chip(
label: Text(
estado.ecualizadorActivo ? AppLocalizations.of(ctx).equalizerActive : AppLocalizations.of(ctx).equalizerDisabled,
),
visualDensity: VisualDensity.compact,
),
],
),
const SizedBox(height: 8),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: Text(AppLocalizations.of(ctx).equalizerEnable),
subtitle: Text(
disponible
? 'Los cambios se aplican en tiempo real a la emisora actual.'
: 'Se guardan los cambios y se aplicarán cuando Android habilite el efecto.',
),
value: estado.ecualizadorActivo,
onChanged: estado.cambiarEcualizadorActivo,
),
if (mostrarModoPorEmisora) ...[
const SizedBox(height: 8),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: Text(AppLocalizations.of(ctx).equalizerPerStationTitle),
subtitle: Text(
usandoEqPropio
? AppLocalizations.of(ctx).equalizerPerStationActive(emisoraActual.nombre)
: AppLocalizations.of(ctx).equalizerPerStationMain(emisoraActual.nombre),
),
value: usandoEqPropio,
onChanged:
(usarPropio) =>
estado.cambiarModoEcualizadorEmisoraActual(
usarPropio: usarPropio,
),
),
],
const SizedBox(height: 8),
PresetsEcualizadorWidget(
presetActual: estado.presetEcualizador,
onSeleccionar: (p) => estado.cambiarPresetEcualizador(p),
),
const SizedBox(height: 12),
EcualizadorWidget(
preset: estado.presetEcualizador,
onCambio: (p) => estado.cambiarPresetEcualizador(p),
),
],
),
);
},
);
}
}
class _SeccionEmisoraPreferida extends StatelessWidget {
const _SeccionEmisoraPreferida();
@override
Widget build(BuildContext context) {
final estado = context.watch<EstadoRadio>();
final favoritas = estado.listaFavoritos;
final preferida = estado.emisoraPreferida;
final opciones = _opciones(estado, preferida);
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.radio_rounded),
const SizedBox(width: 12),
Text(
AppLocalizations.of(context).preferredStationTitle,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 8),
Text(
'Se preselecciona al crear alarmas y puede iniciarse como reproducción rápida.',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
if (opciones.isEmpty)
const ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(Icons.info_outline_rounded),
title: Text('Todavía no hay emisoras disponibles'),
subtitle: Text(
'Guardá favoritas o cargá emisoras para elegir una preferida.',
),
)
else
DropdownButtonFormField<String>(
initialValue: preferida?.uuid,
decoration: InputDecoration(
labelText:
favoritas.isEmpty
? 'Fallback automático'
: 'Favorita por defecto',
),
items: [
for (final emisora in opciones)
DropdownMenuItem<String>(
value: emisora.uuid,
child: Text(
emisora.nombre,
overflow: TextOverflow.ellipsis,
),
),
],
onChanged: (uuid) async {
final seleccion = opciones.firstWhere((e) => e.uuid == uuid);
await context.read<EstadoRadio>().cambiarEmisoraPreferida(
seleccion,
);
},
),
if (preferida != null) ...[
const SizedBox(height: 8),
Text(
favoritas.any((e) => e.uuid == preferida.uuid)
? 'Preferida actual: ${preferida.nombre}'
: 'Sin favoritas: usando automáticamente ${preferida.nombre}',
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: FilledButton.tonalIcon(
icon: const Icon(Icons.play_arrow_rounded),
label: Text(AppLocalizations.of(context).preferredStationPlay),
onPressed:
() => context.read<EstadoRadio>().reproducirEmisoraPreferida(),
),
),
],
],
),
);
}
List<Emisora> _opciones(EstadoRadio estado, Emisora? preferida) {
final base =
estado.listaFavoritos.isNotEmpty
? estado.listaFavoritos
: estado.emisorasDisponiblesPreferencia;
final mapa = <String, Emisora>{for (final emisora in base) emisora.uuid: emisora};
if (preferida != null) {
mapa[preferida.uuid] = preferida;
}
return mapa.values.toList();
}
}
class _SeccionEmisoras extends StatelessWidget {
const _SeccionEmisoras();
@override
Widget build(BuildContext context) {
final estado = context.watch<EstadoRadio>();
final custom = estado.emisorasCustom;
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.add_circle_outline),
const SizedBox(width: 12),
Text(
AppLocalizations.of(context).customStationsTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const Spacer(),
TextButton.icon(
icon: const Icon(Icons.add),
label: const Text('Añadir'),
onPressed: () => _mostrarFormularioAnadir(context),
),
],
),
if (custom.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
AppLocalizations.of(context).customStationsEmpty,
style: const TextStyle(color: Colors.grey),
),
)
else
for (final emisora in custom)
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.radio),
title: Text(emisora.nombre),
subtitle: Text(
emisora.url,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.play_arrow),
tooltip: AppLocalizations.of(context).playAction,
onPressed:
() => context.read<EstadoRadio>().reproducir(emisora),
),
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: AppLocalizations.of(context).deleteAction,
onPressed:
() => context
.read<EstadoRadio>()
.eliminarEmitoraCustom(emisora.uuid),
),
],
),
),
],
),
);
}
Future<void> _mostrarFormularioAnadir(BuildContext context) async {
await showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (ctx) => const _FormularioEmisora(),
);
}
}
class _FormularioEmisora extends StatefulWidget {
const _FormularioEmisora();
@override
State<_FormularioEmisora> createState() => _FormularioEmisoraState();
}
class _FormularioEmisoraState extends State<_FormularioEmisora> {
final _formKey = GlobalKey<FormState>();
final _nombreCtrl = TextEditingController();
final _urlCtrl = TextEditingController();
final _paisCtrl = TextEditingController();
bool _guardando = false;
@override
void dispose() {
_nombreCtrl.dispose();
_urlCtrl.dispose();
_paisCtrl.dispose();
super.dispose();
}
Future<void> _guardar() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _guardando = true);
final emisora = Emisora(
uuid: const Uuid().v4(),
nombre: _nombreCtrl.text.trim(),
url: _urlCtrl.text.trim(),
pais: _paisCtrl.text.trim().isEmpty ? null : _paisCtrl.text.trim(),
);
await context.read<EstadoRadio>().agregarEmitoraCustom(emisora);
if (mounted) Navigator.pop(context);
}
@override
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),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Añadir emisora',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
TextFormField(
controller: _nombreCtrl,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).stationNameLabel,
border: const OutlineInputBorder(),
),
validator:
(v) =>
v == null || v.trim().isEmpty
? AppLocalizations.of(context).requiredField
: null,
),
const SizedBox(height: 12),
TextFormField(
controller: _urlCtrl,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).streamUrlLabel,
hintText: 'http://stream.ejemplo.com:8000/radio',
border: const OutlineInputBorder(),
),
keyboardType: TextInputType.url,
validator: (v) {
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;
},
),
const SizedBox(height: 12),
TextFormField(
controller: _paisCtrl,
decoration: const InputDecoration(
labelText: 'País (opcional)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
FilledButton(
onPressed: _guardando ? null : _guardar,
child:
_guardando
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(AppLocalizations.of(context).saveStation),
),
],
),
),
);
}
}
class _SeccionBackup extends StatelessWidget {
const _SeccionBackup();
Future<void> _exportar(BuildContext context) async {
try {
final estado = context.read<EstadoRadio>();
final config = await estado.exportarConfig();
final json = const JsonEncoder.withIndent(' ').convert(config);
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/pluriwave-backup.json');
await file.writeAsString(json);
await Share.shareXFiles(
[XFile(file.path)],
subject: 'PluriWave — copia de seguridad',
text:
'Configuración de PluriWave exportada el ${DateTime.now().toLocal()}',
);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).backupExportError(e.toString()))));
}
}
}
Future<void> _importar(BuildContext context) async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
);
if (result == null || result.files.single.path == null) return;
final file = File(result.files.single.path!);
final json =
jsonDecode(await file.readAsString()) as Map<String, dynamic>;
if (context.mounted) {
final confirmar = await showDialog<bool>(
context: context,
builder:
(ctx) => AlertDialog(
title: const Text('Importar configuración'),
content: const Text(
'Esto añadirá los favoritos, emisoras y presets del fichero. ¿Continuar?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text(AppLocalizations.of(ctx).cancelAction),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text(AppLocalizations.of(ctx).backupImportTitle),
),
],
),
);
if (confirmar != true) return;
if (context.mounted) {
final estado = context.read<EstadoRadio>();
final messenger = ScaffoldMessenger.of(context);
await estado.importarConfig(json);
messenger.showSnackBar(
const SnackBar(
content: Text('Configuración importada correctamente'),
),
);
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).backupImportError(e.toString()))));
}
}
}
@override
Widget build(BuildContext context) {
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.backup_outlined),
const SizedBox(width: 12),
Text(
AppLocalizations.of(context).backupSectionTitle,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.upload_outlined),
title: const Text('Exportar configuración'),
subtitle: Text(AppLocalizations.of(context).backupExportSubtitle),
onTap: () => _exportar(context),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.download_outlined),
title: const Text('Importar configuración'),
subtitle: Text(AppLocalizations.of(context).backupImportSubtitle),
onTap: () => _importar(context),
),
],
),
);
}
}
class _SeccionInfo extends StatelessWidget {
const _SeccionInfo();
@override
Widget build(BuildContext context) {
return Consumer<EstadoRadio>(
builder:
(ctx, estado, _) => PluriGlassSurface(
child: Column(
children: [
FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (ctx, snap) {
final version =
snap.hasData
? 'v${snap.data!.version}+${snap.data!.buildNumber}'
: 'Cargando versión...';
return ListTile(
contentPadding: EdgeInsets.zero,
leading: const PluriIcon(
glyph: PluriIconGlyph.settings,
variant: PluriIconVariant.filled,
),
title: const Text('PluriWave'),
subtitle: Text(AppLocalizations.of(ctx).appVersionSubtitle(version)),
);
},
),
FutureBuilder<int>(
future: estado.favoritos.obtenerTodos().then((l) => l.length),
builder:
(ctx, snap) => ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.favorite_outline),
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.verified_outlined),
title: Text(AppLocalizations.of(ctx).stationFilterTitle),
subtitle: Text(AppLocalizations.of(ctx).stationFilterSubtitle),
trailing: const Icon(Icons.check_circle, color: Colors.green),
),
const ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(Icons.music_note_outlined),
title: Text('Audio en background'),
subtitle: Text('Continúa al apagar la pantalla'),
trailing: Icon(Icons.check_circle, color: Colors.green),
),
],
),
),
);
}
}
String _formatearDuracionTimer(Duration duracion) {
final horas = duracion.inHours;
final minutos = duracion.inMinutes.remainder(60);
final segundos = duracion.inSeconds.remainder(60);
if (horas > 0) {
return '${horas}h ${minutos.toString().padLeft(2, '0')}m ${segundos.toString().padLeft(2, '0')}s';
}
if (minutos > 0) {
return segundos == 0 ? '$minutos min' : '${minutos}m ${segundos}s';
}
return '$segundos s';
}