feat(v0.3.0): ecualizador + favoritos en tarjeta + emisoras custom + export/import + fix MainActivity
Some checks failed
Flutter CI/CD — PluriWave / Test + Build (push) Has been cancelled
Some checks failed
Flutter CI/CD — PluriWave / Test + Build (push) Has been cancelled
- MainActivity: extiende AudioServiceActivity (fix pantalla en blanco) - ServicioAudio: AndroidEqualizer en AudioPipeline, aplicarPreset(), setBanda() - PresetEcualizador: modelo independiente (Flat/Rock/Pop/BassBoost/Jazz/Voz) - EcualizadorWidget: 5 sliders verticales + PresetsEcualizadorWidget - TarjetaEmisora: botón favorito visible en grid y lista (toggle con SnackBar) - EstadoRadio: emisoras custom (CRUD), export/import JSON v1, presets por emisora - PantallaAjustes: ecualizador interactivo, form añadir emisora, backup export/import - pubspec: +file_picker ^8.1.7, +uuid ^4.5.1
This commit is contained in:
@@ -1,22 +1,24 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../modelos/preset_ecualizador.dart';
|
||||
import '../servicios/servicio_audio.dart';
|
||||
import '../servicios/servicio_favoritos.dart';
|
||||
import '../servicios/servicio_radio.dart';
|
||||
import '../servicios/servicio_timer.dart';
|
||||
|
||||
/// Estado global de la app con ChangeNotifier (Provider).
|
||||
///
|
||||
/// Errores de reproducción se emiten por [errorStream] para mostrar como
|
||||
/// SnackBar — no bloquean la UI.
|
||||
class EstadoRadio extends ChangeNotifier {
|
||||
final ServicioAudio audio = ServicioAudio();
|
||||
final ServicioFavoritos favoritos = ServicioFavoritos();
|
||||
final ServicioRadio radio = ServicioRadio();
|
||||
late final ServicioTimer timer;
|
||||
|
||||
// Errores de reproducción → SnackBar en el UI
|
||||
// Errores de reproducción → SnackBar
|
||||
final _errorController = StreamController<String>.broadcast();
|
||||
Stream<String> get errorStream => _errorController.stream;
|
||||
|
||||
@@ -24,10 +26,15 @@ class EstadoRadio extends ChangeNotifier {
|
||||
List<Emisora> _tendencias = [];
|
||||
List<Emisora> _resultadosBusqueda = [];
|
||||
List<Emisora> _listafavoritos = [];
|
||||
List<Emisora> _emisorasCustom = [];
|
||||
|
||||
// Presets EQ guardados por uuid de emisora
|
||||
final Map<String, PresetEcualizador> _presetsEmisoraMap = {};
|
||||
PresetEcualizador _presetActual = PresetEcualizador.flat;
|
||||
|
||||
bool _cargandoPopulares = false;
|
||||
bool _cargandoBusqueda = false;
|
||||
String? _errorCarga; // solo para errores de carga de lista (banner estático)
|
||||
String? _errorCarga;
|
||||
|
||||
EstadoRadio() {
|
||||
timer = ServicioTimer(audio);
|
||||
@@ -38,16 +45,20 @@ class EstadoRadio extends ChangeNotifier {
|
||||
List<Emisora> get tendencias => _tendencias;
|
||||
List<Emisora> get resultadosBusqueda => _resultadosBusqueda;
|
||||
List<Emisora> get listaFavoritos => _listafavoritos;
|
||||
List<Emisora> get emisorasCustom => _emisorasCustom;
|
||||
bool get cargandoPopulares => _cargandoPopulares;
|
||||
bool get cargandoBusqueda => _cargandoBusqueda;
|
||||
String? get error => _errorCarga;
|
||||
Emisora? get emisoraActual => audio.emisoraActual;
|
||||
Stream<EstadoReproduccion> get estadoStream => audio.estadoStream;
|
||||
PresetEcualizador get presetEcualizador => _presetActual;
|
||||
bool get ecualizadorDisponible => audio.ecualizadorDisponible;
|
||||
|
||||
Future<void> _init() async {
|
||||
await Future.wait([
|
||||
cargarPopulares(),
|
||||
cargarFavoritos(),
|
||||
_cargarEmisoresCustom(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -92,7 +103,6 @@ class EstadoRadio extends ChangeNotifier {
|
||||
tag: tag,
|
||||
);
|
||||
} catch (e) {
|
||||
// Error de búsqueda → toast, no bloquear pantalla
|
||||
_errorController.add('Error en la búsqueda. Comprueba tu conexión.');
|
||||
} finally {
|
||||
_cargandoBusqueda = false;
|
||||
@@ -104,9 +114,11 @@ class EstadoRadio extends ChangeNotifier {
|
||||
try {
|
||||
await audio.reproducir(emisora);
|
||||
radio.registrarClick(emisora.uuid); // fire & forget
|
||||
// Restaurar preset del ecualizador de esta emisora
|
||||
final preset = _presetsEmisoraMap[emisora.uuid] ?? PresetEcualizador.flat;
|
||||
await cambiarPresetEcualizador(preset, guardPorEmisora: false);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
// Error de reproducción → SnackBar, no pintar en medio de la UI
|
||||
_errorController.add('No se puede reproducir "${emisora.nombre}"');
|
||||
}
|
||||
}
|
||||
@@ -119,11 +131,125 @@ class EstadoRadio extends ChangeNotifier {
|
||||
Future<bool> toggleFavorito(Emisora emisora) async {
|
||||
final esFav = await favoritos.toggleFavorito(emisora);
|
||||
await cargarFavoritos();
|
||||
notifyListeners();
|
||||
return esFav;
|
||||
}
|
||||
|
||||
Future<bool> esFavorito(String uuid) => favoritos.esFavorito(uuid);
|
||||
|
||||
// ── Ecualizador ──────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> cambiarPresetEcualizador(
|
||||
PresetEcualizador preset, {
|
||||
bool guardPorEmisora = true,
|
||||
}) async {
|
||||
_presetActual = preset;
|
||||
await audio.aplicarPreset(preset);
|
||||
if (guardPorEmisora && emisoraActual != null) {
|
||||
_presetsEmisoraMap[emisoraActual!.uuid] = preset;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> cambiarBandaEcualizador(int index, double db) async {
|
||||
final bandas = List<double>.from(_presetActual.bandas);
|
||||
if (index >= 0 && index < bandas.length) bandas[index] = db;
|
||||
_presetActual = PresetEcualizador(nombre: 'Personalizado', bandas: bandas);
|
||||
await audio.setBanda(index, db);
|
||||
if (emisoraActual != null) {
|
||||
_presetsEmisoraMap[emisoraActual!.uuid] = _presetActual;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ── Emisoras personalizadas ───────────────────────────────────────────────
|
||||
|
||||
Future<File> _archivoCustom() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
return File('${dir.path}/emisoras_custom.json');
|
||||
}
|
||||
|
||||
Future<void> _cargarEmisoresCustom() async {
|
||||
try {
|
||||
final f = await _archivoCustom();
|
||||
if (!await f.exists()) return;
|
||||
final data = jsonDecode(await f.readAsString()) as List;
|
||||
_emisorasCustom = data
|
||||
.map((e) => Emisora.fromMap(Map<String, dynamic>.from(e as Map)))
|
||||
.toList();
|
||||
} catch (_) {
|
||||
_emisorasCustom = [];
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _guardarEmisoresCustom() async {
|
||||
final f = await _archivoCustom();
|
||||
await f.writeAsString(jsonEncode(_emisorasCustom.map((e) => e.toMap()).toList()));
|
||||
}
|
||||
|
||||
Future<void> agregarEmitoraCustom(Emisora emisora) async {
|
||||
_emisorasCustom.removeWhere((e) => e.uuid == emisora.uuid);
|
||||
_emisorasCustom.add(emisora);
|
||||
await _guardarEmisoresCustom();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> eliminarEmitoraCustom(String uuid) async {
|
||||
_emisorasCustom.removeWhere((e) => e.uuid == uuid);
|
||||
await _guardarEmisoresCustom();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ── Export / Import ───────────────────────────────────────────────────────
|
||||
|
||||
/// Genera el JSON de toda la configuración.
|
||||
Future<Map<String, dynamic>> exportarConfig() async {
|
||||
final favs = await favoritos.obtenerTodos();
|
||||
return {
|
||||
'version': 1,
|
||||
'exportedAt': DateTime.now().toIso8601String(),
|
||||
'favoritos': favs.map((e) => e.toMap()).toList(),
|
||||
'emisorasCustom': _emisorasCustom.map((e) => e.toMap()).toList(),
|
||||
'presetsEcualizador': _presetsEmisoraMap.map(
|
||||
(uuid, preset) => MapEntry(uuid, preset.toJson()),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Importa configuración desde un JSON exportado previamente.
|
||||
Future<void> importarConfig(Map<String, dynamic> data) async {
|
||||
final version = data['version'] as int? ?? 1;
|
||||
if (version != 1) throw Exception('Versión de configuración no compatible');
|
||||
|
||||
// Importar favoritos
|
||||
final favRaw = data['favoritos'] as List? ?? [];
|
||||
for (final raw in favRaw) {
|
||||
final emisora = Emisora.fromMap(Map<String, dynamic>.from(raw as Map));
|
||||
await favoritos.agregar(emisora);
|
||||
}
|
||||
|
||||
// Importar emisoras custom
|
||||
final customRaw = data['emisorasCustom'] as List? ?? [];
|
||||
_emisorasCustom = customRaw
|
||||
.map((e) => Emisora.fromMap(Map<String, dynamic>.from(e as Map)))
|
||||
.toList();
|
||||
await _guardarEmisoresCustom();
|
||||
|
||||
// Importar presets EQ
|
||||
final presetsRaw = data['presetsEcualizador'] as Map? ?? {};
|
||||
_presetsEmisoraMap.clear();
|
||||
presetsRaw.forEach((uuid, presetJson) {
|
||||
_presetsEmisoraMap[uuid as String] =
|
||||
PresetEcualizador.desdeJson(Map<String, dynamic>.from(presetJson as Map));
|
||||
});
|
||||
|
||||
await cargarFavoritos();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void iniciarTimer(int minutos) {
|
||||
timer.iniciar(minutos);
|
||||
notifyListeners();
|
||||
|
||||
Reference in New Issue
Block a user