feat(recording): add safety limits and adaptive headers
Build & Deploy Pluriwave / Análisis de código (push) Successful in 23s
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 1m37s

This commit is contained in:
2026-05-22 15:24:14 +02:00
parent 2320dbdc5f
commit 8190c4ab8d
34 changed files with 1445 additions and 459 deletions
+115 -15
View File
@@ -87,10 +87,12 @@ class _SeccionGrabaciones extends StatelessWidget {
if (ruta == null) return;
try {
await estado.cambiarDirectorioGrabacion(ruta);
if (!context.mounted) return;
messenger.showSnackBar(
const SnackBar(content: Text('Ruta de grabación actualizada')),
SnackBar(content: Text(l10n.recordingsPathUpdated)),
);
} catch (e) {
if (!context.mounted) return;
messenger.showSnackBar(
SnackBar(content: Text(l10n.recordingsPathSaveError(e.toString()))),
);
@@ -100,15 +102,97 @@ class _SeccionGrabaciones extends StatelessWidget {
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(
const SnackBar(content: Text('Se usará la carpeta interna por defecto')),
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(
@@ -119,7 +203,7 @@ class _SeccionGrabaciones extends StatelessWidget {
const Icon(Icons.radio_button_checked),
const SizedBox(width: 12),
Text(
AppLocalizations.of(context).recordingsSectionTitle,
l10n.recordingsSectionTitle,
style: Theme.of(context).textTheme.titleMedium,
),
],
@@ -130,35 +214,51 @@ class _SeccionGrabaciones extends StatelessWidget {
(ctx, snap) => ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.folder_outlined),
title: const Text('Carpeta de grabación'),
title: Text(l10n.recordingsFolderTitle),
subtitle: Text(
snap.data ?? AppLocalizations.of(context).recordingsPathCalculating,
snap.data ?? l10n.recordingsPathCalculating,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onTap: () => _seleccionarRuta(context),
),
),
Row(
Wrap(
spacing: 8,
runSpacing: 8,
children: [
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.folder_open_rounded),
label: Text(AppLocalizations.of(context).recordingsChangePath),
onPressed: () => _seleccionarRuta(context),
),
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),
),
const SizedBox(width: 8),
IconButton.filledTonal(
tooltip: AppLocalizations.of(context).recordingsUseDefaultPath,
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(
AppLocalizations.of(context).recordingsOriginalStreamHint,
l10n.recordingsOriginalStreamHint,
style: Theme.of(context).textTheme.bodySmall,
),
],