refactor(state): extract recording and search state, scope screen rebuilds
- New EstadoGrabacion owns the recording service, subscription, directory/size preferences and open-file actions - New EstadoBusqueda owns search, nearby stations, pagination and the min-bitrate filter - New orden_emisoras.dart with the OrdenEmisoras enum, shared sorter and list identity memoization so context.select comparisons work on derived lists - Large screens (inicio, buscar, favoritos, ajustes, reproductor) consume scoped selects/dedicated notifiers instead of root context.watch<EstadoRadio>, so audio buffer events no longer rebuild whole screens - Remove all 15 TODO(S4b) compat members from EstadoRadio; consumers use the dedicated providers. EstadoRadio drops from ~1121 to 753 lines, keeping playback/stations/favorites orchestration - 8 new tests including a rebuild-scoping probe (110 total green), flutter analyze clean
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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<EstadoGrabacionRadio>? _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<void> 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<void> 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<void> detener() => servicio.detener();
|
||||
|
||||
Future<void> cambiarMaxBytes(int bytes) async {
|
||||
await servicio.guardarMaxBytes(bytes);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> cambiarDirectorio(String path) async {
|
||||
await servicio.guardarDirectorio(path);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> restaurarDirectorio() async {
|
||||
await servicio.limpiarDirectorioConfigurado();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<String> directorioEfectivo() => servicio.directorioEfectivo();
|
||||
|
||||
Future<bool> abrirDirectorio() async {
|
||||
final ruta = await directorioEfectivo();
|
||||
await Directory(ruta).create(recursive: true);
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
final abierto = await _fileActionsChannel.invokeMethod<bool>(
|
||||
'viewDirectory',
|
||||
{'path': ruta},
|
||||
);
|
||||
return abierto ?? false;
|
||||
}
|
||||
final uri = Uri.directory(ruta);
|
||||
return launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
|
||||
Future<bool> 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<bool>('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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user