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. 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 _seleccionarRuta(BuildContext context) async { final estado = context.read(); 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 _restaurarRuta(BuildContext context) async { final estado = context.read(); 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 _abrirCarpeta(BuildContext context) async { final estado = context.read(); 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 _editarTamanoMaximo(BuildContext context) async { final estado = context.read(); final actualMb = _bytesAMegabytes(estado.maxBytes); final nuevoMb = await showModalBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, builder: (ctx) => _HojaTamanoMaximo(actualMb: actualMb), ); if (nuevoMb == null || !context.mounted) return; final l10n = AppLocalizations.of(context); 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(); final l10n = AppLocalizations.of(context); return PluriGlassSurface( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FutureBuilder( 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, ), ], ), ); } } /// The "maximum recording size" bottom sheet's own content, as a /// `StatefulWidget` so its `TextEditingController` is owned by the SHEET's /// `State`, not by the caller's `async` function — same bugfix shape as /// `_HojaEditarGrupo` in `pantalla_ajustes_grupos_favoritos.dart` (a /// controller disposed immediately after `showModalBottomSheet` resolves /// races the sheet's own close animation, which still holds a bound /// `TextField` for a couple more frames). Flutter only calls /// `State.dispose()` once this widget is actually removed from the tree, /// i.e. after the close animation finishes. class _HojaTamanoMaximo extends StatefulWidget { const _HojaTamanoMaximo({required this.actualMb}); final int actualMb; @override State<_HojaTamanoMaximo> createState() => _HojaTamanoMaximoState(); } class _HojaTamanoMaximoState extends State<_HojaTamanoMaximo> { late final TextEditingController _controller = TextEditingController( text: widget.actualMb.toString(), ); @override void dispose() { _controller.dispose(); super.dispose(); } void _guardar() { final value = int.tryParse(_controller.text.trim()); if (value == null || value <= 0) return; Navigator.of(context).pop(value); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final bottom = MediaQuery.viewInsetsOf(context).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(context).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: _guardar, icon: const Icon(Icons.save_rounded), label: Text(l10n.saveQuickAccessButton), ), ], ), ); } }