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 ───────────────────────────────────────────────────────
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user