Files
pluriwave/lib/pantallas/ajustes/pantalla_ajustes_grabaciones.dart
T

218 lines
7.7 KiB
Dart

import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../estado/estado_grabacion.dart';
import '../../l10n/gen/app_localizations.dart';
import '../../widgets/pluri_glass_surface.dart';
import '../../widgets/pluri_layout.dart';
import '../../widgets/pluri_push_scaffold.dart';
/// GRABACIONES Y MÚSICA group · "Grabaciones" (design ADR-3). Body moved
/// verbatim from the former `_SeccionGrabaciones` in `pantalla_ajustes.dart`
/// — only the panel header's icon and title were removed (the pushed
/// screen's title now carries them); every method below is unchanged.
///
/// Known pre-existing bug, deliberately NOT fixed here (out of scope, moved
/// verbatim, tracked separately): [_editarTamanoMaximo] disposes its
/// [TextEditingController] immediately after `showModalBottomSheet` resolves,
/// racing the sheet's own close animation — the same shape of bug already
/// documented for `_editarGrupo` in `pantalla_ajustes_grupos_favoritos.dart`.
class PantallaAjustesGrabaciones extends StatelessWidget {
const PantallaAjustesGrabaciones({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return PluriPushScaffold(
title: l10n.recordingsSectionTitle,
body: ListView(
padding: PluriLayout.pageContentPadding,
children: const [_CuerpoGrabaciones()],
),
);
}
}
class _CuerpoGrabaciones extends StatelessWidget {
const _CuerpoGrabaciones();
Future<void> _seleccionarRuta(BuildContext context) async {
final estado = context.read<EstadoGrabacion>();
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.cambiarDirectorio(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<EstadoGrabacion>();
final messenger = ScaffoldMessenger.of(context);
final l10n = AppLocalizations.of(context);
await estado.restaurarDirectorio();
if (!context.mounted) return;
messenger.showSnackBar(
SnackBar(content: Text(l10n.recordingsDefaultFolderRestored)),
);
}
Future<void> _abrirCarpeta(BuildContext context) async {
final estado = context.read<EstadoGrabacion>();
final messenger = ScaffoldMessenger.of(context);
final l10n = AppLocalizations.of(context);
try {
final abierto = await estado.abrirDirectorio();
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<EstadoGrabacion>();
final l10n = AppLocalizations.of(context);
final actualMb = _bytesAMegabytes(estado.maxBytes);
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.cambiarMaxBytes(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) {
// Recording state lives in EstadoGrabacion (S4-R2): this section only
// rebuilds on recording changes, never on playback notifications.
final estado = context.watch<EstadoGrabacion>();
final l10n = AppLocalizations.of(context);
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FutureBuilder<String>(
future: estado.directorioEfectivo(),
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.maxBytes)),
),
onTap: () => _editarTamanoMaximo(context),
),
const SizedBox(height: 8),
Text(
l10n.recordingsOriginalStreamHint,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
}