diff --git a/lib/estado/estado_radio.dart b/lib/estado/estado_radio.dart index 1095c07..e355039 100644 --- a/lib/estado/estado_radio.dart +++ b/lib/estado/estado_radio.dart @@ -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> 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 : 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 importarConfig(Map 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.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.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) { + // 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().map((n) => n.toInt()).toList(), + ); + } + } + await cargarFavoritos(); notifyListeners(); } diff --git a/lib/servicios/servicio_favoritos.dart b/lib/servicios/servicio_favoritos.dart index 538444b..0f48f61 100644 --- a/lib/servicios/servicio_favoritos.dart +++ b/lib/servicios/servicio_favoritos.dart @@ -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 restaurarGrupo(GrupoFavoritos grupo) async { + if (grupo.esSinAsignar) return; + final db = await _database; + await db.insert( + 'grupos_favoritos', + grupo.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + Future eliminarGrupo(String id) async { if (id == GrupoFavoritos.sinAsignarId) return; final db = await _database; diff --git a/openspec/config.yaml b/openspec/config.yaml index b52c35d..11349e3 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -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