Exportar e importar absolutamente toda la información de las preferencias de la aplicación
This commit is contained in:
@@ -899,32 +899,74 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
|
|
||||||
// ── Export / Import ───────────────────────────────────────────────────────
|
// ── 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 {
|
Future<Map<String, dynamic>> exportarConfig() async {
|
||||||
final favs = await favoritos.obtenerTodos();
|
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 {
|
return {
|
||||||
'version': 1,
|
'version': 2,
|
||||||
'exportedAt': DateTime.now().toIso8601String(),
|
'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(),
|
'favoritos': favs.map((e) => e.toMap()).toList(),
|
||||||
|
// Emisoras personalizadas
|
||||||
'emisorasCustom': _emisorasCustom.map((e) => e.toMap()).toList(),
|
'emisorasCustom': _emisorasCustom.map((e) => e.toMap()).toList(),
|
||||||
|
// Ecualizador
|
||||||
'presetPrincipalEcualizador': _presetPrincipal.toJson(),
|
'presetPrincipalEcualizador': _presetPrincipal.toJson(),
|
||||||
'presetsEcualizador': _presetsEmisoraMap.map(
|
'presetsEcualizador': _presetsEmisoraMap.map(
|
||||||
(uuid, preset) => MapEntry(uuid, preset.toJson()),
|
(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.
|
/// 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 {
|
Future<void> importarConfig(Map<String, dynamic> data) async {
|
||||||
final version = data['version'] as int? ?? 1;
|
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? ?? [];
|
final favRaw = data['favoritos'] as List? ?? [];
|
||||||
for (final raw in favRaw) {
|
for (final raw in favRaw) {
|
||||||
final emisora = Emisora.fromMap(Map<String, dynamic>.from(raw as Map));
|
final emisora = Emisora.fromMap(Map<String, dynamic>.from(raw as Map));
|
||||||
await favoritos.agregar(emisora);
|
await favoritos.agregar(emisora);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Emisoras custom ───────────────────────────────────────────────────
|
||||||
final customRaw = data['emisorasCustom'] as List? ?? [];
|
final customRaw = data['emisorasCustom'] as List? ?? [];
|
||||||
_emisorasCustom =
|
_emisorasCustom =
|
||||||
customRaw
|
customRaw
|
||||||
@@ -932,6 +974,7 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
.toList();
|
.toList();
|
||||||
await _guardarEmisorasCustom();
|
await _guardarEmisorasCustom();
|
||||||
|
|
||||||
|
// ── Ecualizador ───────────────────────────────────────────────────────
|
||||||
final principalRaw = data['presetPrincipalEcualizador'];
|
final principalRaw = data['presetPrincipalEcualizador'];
|
||||||
if (principalRaw is Map) {
|
if (principalRaw is Map) {
|
||||||
_presetPrincipal = PresetEcualizador.desdeJson(
|
_presetPrincipal = PresetEcualizador.desdeJson(
|
||||||
@@ -968,6 +1011,42 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
actual == null ? _presetPrincipal : _presetParaEmisora(actual.uuid);
|
actual == null ? _presetPrincipal : _presetParaEmisora(actual.uuid);
|
||||||
await _aplicarPresetActivo(presetActivo);
|
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();
|
await cargarFavoritos();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
Future<void> eliminarGrupo(String id) async {
|
||||||
if (id == GrupoFavoritos.sinAsignarId) return;
|
if (id == GrupoFavoritos.sinAsignarId) return;
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
schema: spec-driven
|
schema: spec-driven
|
||||||
|
|
||||||
context: |
|
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.
|
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.
|
Style: flutter_lints via analysis_options.yaml; use flutter analyze and dart format.
|
||||||
Constraint: never run flutter build after changes.
|
Constraint: never run flutter build after changes.
|
||||||
|
|
||||||
@@ -11,7 +14,7 @@ strict_tdd: true
|
|||||||
|
|
||||||
testing:
|
testing:
|
||||||
strict_tdd: true
|
strict_tdd: true
|
||||||
detected: 2026-04-27
|
detected: 2026-06-04
|
||||||
test_runner:
|
test_runner:
|
||||||
framework: flutter_test
|
framework: flutter_test
|
||||||
command: flutter test
|
command: flutter test
|
||||||
|
|||||||
Reference in New Issue
Block a user