import 'dart:async'; import 'dart:io'; import 'dart:ui' show Locale; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; import '../l10n/gen/app_localizations.dart'; import '../modelos/emisora.dart'; import '../servicios/servicio_grabacion_radio.dart'; /// Recording state extracted from `EstadoRadio` (S4-R2). /// /// Owns [ServicioGrabacionRadio] and the recording-state subscription, and /// notifies ONLY its own listeners — recording progress must not rebuild /// `EstadoRadio` consumers (S4-R5). Playback orchestration (stop recording on /// pause/stop/station switch) stays in `EstadoRadio`, which keeps a reference /// to this notifier. class EstadoGrabacion extends ChangeNotifier { EstadoGrabacion({ ServicioGrabacionRadio? servicio, Emisora? Function()? emisoraActual, void Function(String mensaje)? alError, }) : servicio = servicio ?? ServicioGrabacionRadio(), _emisoraActual = emisoraActual ?? (() => null), _alError = alError { _suscripcion = this.servicio.estadoStream.listen((estado) { if (estado.tipo == EstadoGrabacionRadioTipo.error && estado.error != null) { _alError?.call(_textos.radioRecordingError(estado.error!)); } notifyListeners(); }); } static const MethodChannel _fileActionsChannel = MethodChannel( 'pluriwave/file_actions', ); final ServicioGrabacionRadio servicio; /// Callback into the owner (EstadoRadio) for the currently playing station; /// keeps this notifier free of any station-list coupling. final Emisora? Function() _emisoraActual; /// User-visible error sink (EstadoRadio routes it to its snackbar stream). final void Function(String mensaje)? _alError; StreamSubscription? _suscripcion; AppLocalizations? _l10n; AppLocalizations get _textos { final actual = _l10n; if (actual != null) return actual; return lookupAppLocalizations(const Locale('es')); } void configurarLocalizaciones(AppLocalizations l10n) { _l10n = l10n; servicio.configurarLocalizaciones(l10n); } Future inicializar() => servicio.inicializar(); EstadoGrabacionRadio get estado => servicio.estado; bool get activa => servicio.estado.activa; String? get directorioConfigurado => servicio.directorioConfigurado; int get maxBytes => servicio.maxBytes; File? get ultimoArchivo => servicio.ultimoArchivo; Future iniciar({Duration? duracion}) async { final actual = _emisoraActual(); if (actual == null) { _alError?.call(_textos.recordingSelectStationFirst); return; } try { await servicio.iniciar(actual, duracion: duracion); } catch (e) { _alError?.call(_textos.recordingStartError(e.toString())); } } Future detener() => servicio.detener(); Future cambiarMaxBytes(int bytes) async { await servicio.guardarMaxBytes(bytes); notifyListeners(); } Future cambiarDirectorio(String path) async { await servicio.guardarDirectorio(path); notifyListeners(); } Future restaurarDirectorio() async { await servicio.limpiarDirectorioConfigurado(); notifyListeners(); } Future directorioEfectivo() => servicio.directorioEfectivo(); Future abrirDirectorio() async { final ruta = await directorioEfectivo(); await Directory(ruta).create(recursive: true); if (!kIsWeb && Platform.isAndroid) { final abierto = await _fileActionsChannel.invokeMethod( 'viewDirectory', {'path': ruta}, ); return abierto ?? false; } final uri = Uri.directory(ruta); return launchUrl(uri, mode: LaunchMode.externalApplication); } Future abrirUltimaGrabacion() async { final archivo = ultimoArchivo; if (archivo == null || !await archivo.exists()) { debugPrint('[PluriWave][recordings] last recording missing'); return false; } debugPrint('[PluriWave][recordings] opening last file: ${archivo.path}'); if (!kIsWeb && Platform.isAndroid) { final abierto = await _fileActionsChannel.invokeMethod('openFile', { 'path': archivo.path, 'mimeType': 'audio/*', }); return abierto ?? false; } return launchUrl( Uri.file(archivo.path), mode: LaunchMode.externalApplication, ); } @override void dispose() { _suscripcion?.cancel(); unawaited(servicio.dispose()); super.dispose(); } }