1394 lines
46 KiB
Dart
1394 lines
46 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 '../modelos/grupo_favoritos.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_onboarding_dialog.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),
|
||
_SeccionOrdenListas(),
|
||
SizedBox(height: 12),
|
||
_SeccionGruposFavoritos(),
|
||
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);
|
||
if (!context.mounted) return;
|
||
messenger.showSnackBar(
|
||
SnackBar(content: Text(l10n.recordingsPathUpdated)),
|
||
);
|
||
} catch (e) {
|
||
if (!context.mounted) return;
|
||
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);
|
||
final l10n = AppLocalizations.of(context);
|
||
await estado.restaurarDirectorioGrabacion();
|
||
if (!context.mounted) return;
|
||
messenger.showSnackBar(
|
||
SnackBar(content: Text(l10n.recordingsDefaultFolderRestored)),
|
||
);
|
||
}
|
||
|
||
Future<void> _abrirCarpeta(BuildContext context) async {
|
||
final estado = context.read<EstadoRadio>();
|
||
final messenger = ScaffoldMessenger.of(context);
|
||
final l10n = AppLocalizations.of(context);
|
||
try {
|
||
final abierto = await estado.abrirDirectorioGrabacion();
|
||
if (!context.mounted) return;
|
||
if (!abierto) {
|
||
messenger.showSnackBar(
|
||
SnackBar(content: Text(l10n.recordingsOpenFolderError(l10n.dash))),
|
||
);
|
||
}
|
||
} catch (e) {
|
||
if (!context.mounted) return;
|
||
messenger.showSnackBar(
|
||
SnackBar(content: Text(l10n.recordingsOpenFolderError(e.toString()))),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _editarTamanoMaximo(BuildContext context) async {
|
||
final estado = context.read<EstadoRadio>();
|
||
final l10n = AppLocalizations.of(context);
|
||
final actualMb = _bytesAMegabytes(estado.maxBytesGrabacion);
|
||
final controller = TextEditingController(text: actualMb.toString());
|
||
|
||
final nuevoMb = await showModalBottomSheet<int>(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
showDragHandle: true,
|
||
builder: (ctx) {
|
||
final bottom = MediaQuery.viewInsetsOf(ctx).bottom;
|
||
return Padding(
|
||
padding: EdgeInsets.fromLTRB(20, 0, 20, bottom + 24),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
l10n.recordingsMaxSizeDialogTitle,
|
||
style: Theme.of(ctx).textTheme.titleLarge,
|
||
),
|
||
const SizedBox(height: 16),
|
||
TextField(
|
||
controller: controller,
|
||
autofocus: true,
|
||
keyboardType: TextInputType.number,
|
||
decoration: InputDecoration(
|
||
labelText: l10n.recordingsMaxSizeMbLabel,
|
||
border: const OutlineInputBorder(),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
FilledButton.icon(
|
||
onPressed: () {
|
||
final value = int.tryParse(controller.text.trim());
|
||
if (value == null || value <= 0) return;
|
||
Navigator.of(ctx).pop(value);
|
||
},
|
||
icon: const Icon(Icons.save_rounded),
|
||
label: Text(l10n.saveQuickAccessButton),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
controller.dispose();
|
||
if (nuevoMb == null || !context.mounted) return;
|
||
await estado.cambiarMaxBytesGrabacion(nuevoMb * 1024 * 1024);
|
||
if (!context.mounted) return;
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text(l10n.recordingsMaxSizeSaved(nuevoMb))),
|
||
);
|
||
}
|
||
|
||
int _bytesAMegabytes(int bytes) =>
|
||
(bytes / (1024 * 1024)).round().clamp(1, 1048576);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final estado = context.watch<EstadoRadio>();
|
||
final l10n = AppLocalizations.of(context);
|
||
|
||
return PluriGlassSurface(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.radio_button_checked),
|
||
const SizedBox(width: 12),
|
||
Text(
|
||
l10n.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: Text(l10n.recordingsFolderTitle),
|
||
subtitle: Text(
|
||
snap.data ?? l10n.recordingsPathCalculating,
|
||
maxLines: 2,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
),
|
||
Wrap(
|
||
spacing: 8,
|
||
runSpacing: 8,
|
||
children: [
|
||
OutlinedButton.icon(
|
||
icon: const Icon(Icons.folder_open_rounded),
|
||
label: Text(l10n.recordingsChangePath),
|
||
onPressed: () => _seleccionarRuta(context),
|
||
),
|
||
FilledButton.tonalIcon(
|
||
icon: const Icon(Icons.folder_copy_rounded),
|
||
label: Text(l10n.recordingsOpenFolder),
|
||
onPressed: () => _abrirCarpeta(context),
|
||
),
|
||
IconButton.filledTonal(
|
||
tooltip: l10n.recordingsUseDefaultPath,
|
||
icon: const Icon(Icons.restore_rounded),
|
||
onPressed: () => _restaurarRuta(context),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.sd_storage_rounded),
|
||
title: Text(l10n.recordingsMaxSizeTitle),
|
||
subtitle: Text(
|
||
l10n.recordingsMaxSizeSubtitle(
|
||
_bytesAMegabytes(estado.maxBytesGrabacion),
|
||
),
|
||
),
|
||
onTap: () => _editarTamanoMaximo(context),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
l10n.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 _idiomas = [
|
||
_IdiomaDisponible(Locale('en'), 'English'),
|
||
_IdiomaDisponible(Locale('es'), 'Español'),
|
||
_IdiomaDisponible(Locale('zh'), '中文'),
|
||
_IdiomaDisponible(Locale('hi'), 'हिन्दी'),
|
||
_IdiomaDisponible(Locale('ar'), 'العربية'),
|
||
_IdiomaDisponible(Locale('pt'), 'Português'),
|
||
_IdiomaDisponible(Locale('fr'), 'Français'),
|
||
_IdiomaDisponible(Locale('ru'), 'Русский'),
|
||
_IdiomaDisponible(Locale('de'), 'Deutsch'),
|
||
_IdiomaDisponible(Locale('ja'), '日本語'),
|
||
_IdiomaDisponible(Locale('id'), 'Bahasa Indonesia'),
|
||
_IdiomaDisponible(Locale('bn'), 'বাংলা'),
|
||
_IdiomaDisponible(Locale('it'), 'Italiano'),
|
||
];
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final l10n = AppLocalizations.of(context);
|
||
final estadoIdioma = context.watch<EstadoIdioma>();
|
||
final locale = estadoIdioma.localeSeleccionado;
|
||
final valorActual = locale == null ? _codigoSistema : _codigoLocale(locale);
|
||
|
||
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),
|
||
),
|
||
for (final idioma in _idiomas)
|
||
DropdownMenuItem(
|
||
value: _codigoLocale(idioma.locale),
|
||
child: Text(idioma.nombreNativo),
|
||
),
|
||
],
|
||
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 idioma = _idiomas.firstWhere(
|
||
(item) => _codigoLocale(item.locale) == codigo,
|
||
orElse: () => _idiomas.first,
|
||
);
|
||
await context.read<EstadoIdioma>().seleccionarLocale(
|
||
idioma.locale,
|
||
);
|
||
if (!context.mounted) return;
|
||
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(l10n.languageUpdated(idioma.nombreNativo)),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
static String _codigoLocale(Locale locale) {
|
||
final countryCode = locale.countryCode;
|
||
if (countryCode == null || countryCode.isEmpty) {
|
||
return locale.languageCode;
|
||
}
|
||
return '${locale.languageCode}_$countryCode';
|
||
}
|
||
}
|
||
|
||
class _IdiomaDisponible {
|
||
const _IdiomaDisponible(this.locale, this.nombreNativo);
|
||
|
||
final Locale locale;
|
||
final String nombreNativo;
|
||
}
|
||
|
||
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 l10n = AppLocalizations.of(ctx);
|
||
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(
|
||
l10n.equalizerTitle,
|
||
style: Theme.of(ctx).textTheme.titleMedium,
|
||
),
|
||
const Spacer(),
|
||
Chip(
|
||
label: Text(
|
||
estado.ecualizadorActivo
|
||
? l10n.equalizerActive
|
||
: l10n.equalizerDisabled,
|
||
),
|
||
visualDensity: VisualDensity.compact,
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
SwitchListTile.adaptive(
|
||
contentPadding: EdgeInsets.zero,
|
||
title: Text(l10n.equalizerEnable),
|
||
subtitle: Text(
|
||
disponible
|
||
? l10n.equalizerRealtimeSubtitle
|
||
: l10n.equalizerPendingSubtitle,
|
||
),
|
||
value: estado.ecualizadorActivo,
|
||
onChanged: estado.cambiarEcualizadorActivo,
|
||
),
|
||
if (mostrarModoPorEmisora) ...[
|
||
const SizedBox(height: 8),
|
||
SwitchListTile.adaptive(
|
||
contentPadding: EdgeInsets.zero,
|
||
title: Text(l10n.equalizerPerStationTitle),
|
||
subtitle: Text(
|
||
usandoEqPropio
|
||
? l10n.equalizerPerStationActive(emisoraActual.nombre)
|
||
: l10n.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 _SeccionOrdenListas extends StatelessWidget {
|
||
const _SeccionOrdenListas();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final estado = context.watch<EstadoRadio>();
|
||
final l10n = AppLocalizations.of(context);
|
||
return PluriGlassSurface(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.sort_rounded),
|
||
const SizedBox(width: 12),
|
||
Text(
|
||
l10n.stationOrderTitle,
|
||
style: Theme.of(context).textTheme.titleMedium,
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
SegmentedButton<OrdenEmisoras>(
|
||
segments: [
|
||
ButtonSegment(
|
||
value: OrdenEmisoras.nombre,
|
||
icon: const Icon(Icons.sort_by_alpha_rounded),
|
||
label: Text(l10n.stationOrderByName),
|
||
),
|
||
ButtonSegment(
|
||
value: OrdenEmisoras.calidad,
|
||
icon: const Icon(Icons.hd_rounded),
|
||
label: Text(l10n.stationOrderByQuality),
|
||
),
|
||
],
|
||
selected: {estado.ordenListas},
|
||
onSelectionChanged: (value) {
|
||
estado.cambiarOrdenListas(value.first);
|
||
},
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
l10n.stationOrderScopeDescription,
|
||
style: Theme.of(context).textTheme.bodySmall,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SeccionGruposFavoritos extends StatelessWidget {
|
||
const _SeccionGruposFavoritos();
|
||
|
||
Future<void> _editarGrupo(
|
||
BuildContext context, [
|
||
GrupoFavoritos? grupo,
|
||
]) async {
|
||
final l10n = AppLocalizations.of(context);
|
||
final controller = TextEditingController(text: grupo?.nombre ?? '');
|
||
final nombre = await showModalBottomSheet<String>(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
showDragHandle: true,
|
||
builder: (ctx) {
|
||
final bottom = MediaQuery.viewInsetsOf(ctx).bottom;
|
||
return Padding(
|
||
padding: EdgeInsets.fromLTRB(20, 0, 20, bottom + 24),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
grupo == null
|
||
? l10n.favoriteGroupsAdd
|
||
: l10n.favoriteGroupsEdit,
|
||
style: Theme.of(ctx).textTheme.titleLarge,
|
||
),
|
||
const SizedBox(height: 16),
|
||
TextField(
|
||
controller: controller,
|
||
autofocus: true,
|
||
maxLength: 28,
|
||
decoration: InputDecoration(
|
||
labelText: l10n.favoriteGroupsNameLabel,
|
||
helperText: l10n.favoriteGroupsNameTooLong,
|
||
border: const OutlineInputBorder(),
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
FilledButton.icon(
|
||
icon: const Icon(Icons.save_rounded),
|
||
label: Text(AppLocalizations.of(ctx).saveQuickAccessButton),
|
||
onPressed: () {
|
||
final value = controller.text.trim();
|
||
if (value.isEmpty || value.length > 28) return;
|
||
Navigator.pop(ctx, value);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
controller.dispose();
|
||
if (nombre == null || !context.mounted) return;
|
||
final estado = context.read<EstadoRadio>();
|
||
if (grupo == null) {
|
||
await estado.crearGrupoFavoritos(nombre);
|
||
} else {
|
||
await estado.renombrarGrupoFavoritos(grupo.id, nombre);
|
||
}
|
||
if (!context.mounted) return;
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(
|
||
grupo == null
|
||
? l10n.favoriteGroupsCreated
|
||
: l10n.favoriteGroupsUpdated,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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)));
|
||
}
|
||
|
||
String _nombreVisible(AppLocalizations l10n, GrupoFavoritos grupo) =>
|
||
grupo.esSinAsignar ? l10n.favoriteGroupsUnassigned : grupo.nombre;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final estado = context.watch<EstadoRadio>();
|
||
final l10n = AppLocalizations.of(context);
|
||
final grupos = estado.gruposFavoritos;
|
||
return PluriGlassSurface(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.playlist_add_check_circle_rounded),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Text(
|
||
l10n.favoriteGroupsTitle,
|
||
style: Theme.of(context).textTheme.titleMedium,
|
||
),
|
||
),
|
||
TextButton.icon(
|
||
icon: const Icon(Icons.add_rounded),
|
||
label: Text(l10n.favoriteGroupsAdd),
|
||
onPressed: () => _editarGrupo(context),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(l10n.favoriteGroupsDescription),
|
||
const SizedBox(height: 8),
|
||
for (final grupo in grupos)
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: Icon(
|
||
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),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SeccionEmisoraPreferida extends StatelessWidget {
|
||
const _SeccionEmisoraPreferida();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final estado = context.watch<EstadoRadio>();
|
||
final l10n = AppLocalizations.of(context);
|
||
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(
|
||
l10n.preferredStationTitle,
|
||
style: Theme.of(context).textTheme.titleMedium,
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
l10n.preferredStationDescription,
|
||
style: Theme.of(context).textTheme.bodySmall,
|
||
),
|
||
const SizedBox(height: 12),
|
||
if (opciones.isEmpty)
|
||
ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.info_outline_rounded),
|
||
title: Text(l10n.preferredStationNoStationsTitle),
|
||
subtitle: Text(l10n.preferredStationNoStationsSubtitle),
|
||
)
|
||
else
|
||
DropdownButtonFormField<String>(
|
||
initialValue: preferida?.uuid,
|
||
decoration: InputDecoration(
|
||
labelText:
|
||
favoritas.isEmpty
|
||
? l10n.preferredStationAutomaticFallback
|
||
: l10n.preferredStationDefaultFavorite,
|
||
),
|
||
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)
|
||
? l10n.preferredStationCurrent(preferida.nombre)
|
||
: l10n.preferredStationAutoUsing(preferida.nombre),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: FilledButton.tonalIcon(
|
||
icon: const Icon(Icons.play_arrow_rounded),
|
||
label: Text(l10n.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.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,
|
||
),
|
||
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 _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);
|
||
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';
|
||
}
|