Exportar e importar absolutamente toda la información de las preferencias de la aplicación
Build & Deploy PluriWave / Análisis de código (push) Successful in 38s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m25s

This commit is contained in:
Javier Bautista Fernández
2026-06-04 16:05:58 +02:00
parent 957615dcd6
commit cf9422dff3
3 changed files with 102 additions and 7 deletions
+82 -3
View File
@@ -899,32 +899,74 @@ class EstadoRadio extends ChangeNotifier {
// ── Export / Import ───────────────────────────────────────────────────────
/// Genera el JSON de toda la configuración.
static const _keyAlarmasConfig = 'alarmas_musicales_v1';
/// Genera el JSON de toda la configuración (v2 — portabilidad completa).
Future<Map<String, dynamic>> exportarConfig() async {
final favs = await favoritos.obtenerTodos();
final grupos = await favoritos.obtenerGrupos();
final prefs = await SharedPreferences.getInstance();
// Alarmas: leemos el JSON crudo de SharedPreferences para no duplicar
// lógica de ServicioAlarmas y evitar inyectar una dependencia nueva.
final alarmasRaw = prefs.getString(_keyAlarmasConfig);
final alarmasData =
alarmasRaw != null ? jsonDecode(alarmasRaw) as Map<String, dynamic> : null;
return {
'version': 1,
'version': 2,
'exportedAt': DateTime.now().toIso8601String(),
// Favoritos + grupos (preserva asignaciones grupo_id en cada emisora)
'gruposFavoritos':
grupos
.where((g) => !g.esSinAsignar)
.map((g) => g.toMap())
.toList(),
'favoritos': favs.map((e) => e.toMap()).toList(),
// Emisoras personalizadas
'emisorasCustom': _emisorasCustom.map((e) => e.toMap()).toList(),
// Ecualizador
'presetPrincipalEcualizador': _presetPrincipal.toJson(),
'presetsEcualizador': _presetsEmisoraMap.map(
(uuid, preset) => MapEntry(uuid, preset.toJson()),
),
// Alarmas completas (alarmas + vacaciones + excepciones)
'alarmas': alarmasData,
// Preferencias de usuario
'emisoraPreferidaUuid': _emisoraPreferidaUuid,
'ordenListas': _ordenListas.name,
'timerSuenoPresetsSegundos': _timerSuenoPresetsSegundos,
};
}
/// Importa configuración desde un JSON exportado previamente.
/// Soporta v1 (sin grupos, sin alarmas) y v2 (portabilidad completa).
Future<void> importarConfig(Map<String, dynamic> data) async {
final version = data['version'] as int? ?? 1;
if (version != 1) throw Exception(_textos.unsupportedConfigVersion);
if (version > 2) throw Exception(_textos.unsupportedConfigVersion);
final prefs = await SharedPreferences.getInstance();
// ── Grupos de favoritos (v2) ──────────────────────────────────────────
// Restauramos primero para que al agregar favoritos ya existan los grupos.
if (version >= 2) {
final gruposRaw = data['gruposFavoritos'] as List? ?? [];
for (final raw in gruposRaw) {
final g = GrupoFavoritos.fromMap(Map<String, dynamic>.from(raw as Map));
// Usamos insert directo para preservar id, orden y nombre originales.
await favoritos.restaurarGrupo(g);
}
await cargarGruposFavoritos();
}
// ── 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);
}
// ── Emisoras custom ───────────────────────────────────────────────────
final customRaw = data['emisorasCustom'] as List? ?? [];
_emisorasCustom =
customRaw
@@ -932,6 +974,7 @@ class EstadoRadio extends ChangeNotifier {
.toList();
await _guardarEmisorasCustom();
// ── Ecualizador ───────────────────────────────────────────────────────
final principalRaw = data['presetPrincipalEcualizador'];
if (principalRaw is Map) {
_presetPrincipal = PresetEcualizador.desdeJson(
@@ -968,6 +1011,42 @@ class EstadoRadio extends ChangeNotifier {
actual == null ? _presetPrincipal : _presetParaEmisora(actual.uuid);
await _aplicarPresetActivo(presetActivo);
// ── Alarmas (v2) ──────────────────────────────────────────────────────
if (version >= 2) {
final alarmasData = data['alarmas'];
if (alarmasData is Map<String, dynamic>) {
// Escribimos el bloque JSON tal como estaba en el dispositivo origen.
// ServicioAlarmas lo leerá con su propio fromJson al siguiente acceso.
await prefs.setString(_keyAlarmasConfig, jsonEncode(alarmasData));
}
}
// ── Preferencias de usuario (v2) ──────────────────────────────────────
if (version >= 2) {
final preferidaUuid = data['emisoraPreferidaUuid'] as String?;
_emisoraPreferidaUuid = preferidaUuid;
if (preferidaUuid == null) {
await prefs.remove(_keyEmisoraPreferida);
} else {
await prefs.setString(_keyEmisoraPreferida, preferidaUuid);
}
final ordenRaw = data['ordenListas'] as String?;
_ordenListas = switch (ordenRaw) {
'nombre' => OrdenEmisoras.nombre,
'calidad' => OrdenEmisoras.calidad,
_ => OrdenEmisoras.calidad,
};
await prefs.setString(_keyOrdenListas, _ordenListas.name);
final timerPresetsRaw = data['timerSuenoPresetsSegundos'] as List?;
if (timerPresetsRaw != null) {
await guardarTimerSuenoPresetsSegundos(
timerPresetsRaw.whereType<num>().map((n) => n.toInt()).toList(),
);
}
}
await cargarFavoritos();
notifyListeners();
}
+13
View File
@@ -199,6 +199,19 @@ class ServicioFavoritos {
);
}
/// Restaura un grupo tal como estaba en el dispositivo de origen.
/// Hace un upsert preservando id, nombre y orden originales.
/// Usado exclusivamente por importarConfig para garantizar portabilidad completa.
Future<void> restaurarGrupo(GrupoFavoritos grupo) async {
if (grupo.esSinAsignar) return;
final db = await _database;
await db.insert(
'grupos_favoritos',
grupo.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<void> eliminarGrupo(String id) async {
if (id == GrupoFavoritos.sinAsignarId) return;
final db = await _database;
+7 -4
View File
@@ -1,9 +1,12 @@
schema: spec-driven
schema: spec-driven
context: |
Tech stack: Flutter/Dart app for Android+iOS.
Tech stack: Flutter/Dart app for Android+iOS. Version 0.1.59+60. Dart SDK ^3.7.0.
Architecture: Provider/ChangeNotifier with Spanish domain folders: estado, modelos, pantallas, servicios, widgets.
Testing: flutter_test via `flutter test`; Strict TDD enabled.
Core deps: just_audio, audio_service, audio_session, provider, sqflite, shared_preferences, http,
google_fonts, flutter_animate, cached_network_image, shimmer, share_plus, file_picker, uuid,
url_launcher, geolocator, geocoding, package_info_plus, path_provider.
Testing: flutter_test via `flutter test`; Strict TDD enabled. Dev: sqflite_common_ffi for unit tests.
Style: flutter_lints via analysis_options.yaml; use flutter analyze and dart format.
Constraint: never run flutter build after changes.
@@ -11,7 +14,7 @@ strict_tdd: true
testing:
strict_tdd: true
detected: 2026-04-27
detected: 2026-06-04
test_runner:
framework: flutter_test
command: flutter test