From a3444d7c49d0e159c2c856f6777aba7870c7b3d2 Mon Sep 17 00:00:00 2001 From: agent-dev-bd Date: Sat, 4 Apr 2026 16:28:18 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(bd):=20a=C3=B1adir=20modelo=20Emisora?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modelos/emisora.dart | 97 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 lib/modelos/emisora.dart diff --git a/lib/modelos/emisora.dart b/lib/modelos/emisora.dart new file mode 100644 index 0000000..82957ac --- /dev/null +++ b/lib/modelos/emisora.dart @@ -0,0 +1,97 @@ +/// Modelo de datos de una emisora de radio. +/// +/// Representa una emisora favorita almacenada en SQLite. +/// Los campos opcionales (favicon, pais, idioma, tags) pueden ser null +/// cuando la emisora no dispone de esa información. +class Emisora { + final int? id; + final String uuid; + final String nombre; + final String url; + final String? favicon; + final String? pais; + final String? idioma; + final String? tags; + final int orden; + + const Emisora({ + this.id, + required this.uuid, + required this.nombre, + required this.url, + this.favicon, + this.pais, + this.idioma, + this.tags, + this.orden = 0, + }); + + /// Construye una [Emisora] desde una fila de la tabla `favoritos`. + factory Emisora.fromMap(Map map) { + return Emisora( + id: map['id'] as int?, + uuid: map['uuid'] as String, + nombre: map['nombre'] as String, + url: map['url'] as String, + favicon: map['favicon'] as String?, + pais: map['pais'] as String?, + idioma: map['idioma'] as String?, + tags: map['tags'] as String?, + orden: map['orden'] as int? ?? 0, + ); + } + + /// Serializa la emisora para inserción/actualización en SQLite. + /// No incluye [id] — lo gestiona la BD. + Map toMap() { + return { + 'uuid': uuid, + 'nombre': nombre, + 'url': url, + 'favicon': favicon, + 'pais': pais, + 'idioma': idioma, + 'tags': tags, + 'orden': orden, + }; + } + + /// Devuelve una copia con los campos indicados modificados. + Emisora copyWith({ + int? id, + String? uuid, + String? nombre, + String? url, + String? favicon, + String? pais, + String? idioma, + String? tags, + int? orden, + }) { + return Emisora( + id: id ?? this.id, + uuid: uuid ?? this.uuid, + nombre: nombre ?? this.nombre, + url: url ?? this.url, + favicon: favicon ?? this.favicon, + pais: pais ?? this.pais, + idioma: idioma ?? this.idioma, + tags: tags ?? this.tags, + orden: orden ?? this.orden, + ); + } + + @override + String toString() => + 'Emisora(id: $id, uuid: $uuid, nombre: $nombre, orden: $orden)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Emisora && + runtimeType == other.runtimeType && + uuid == other.uuid; + + @override + int get hashCode => uuid.hashCode; +} From f818cbc43a9fcbbf4f1e7f9c69a1884cc17b16f2 Mon Sep 17 00:00:00 2001 From: agent-dev-bd Date: Sat, 4 Apr 2026 16:28:19 +0200 Subject: [PATCH 2/2] feat(bd): implementar ServicioFavoritos con sqflite --- lib/servicios/servicio_favoritos.dart | 251 ++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 lib/servicios/servicio_favoritos.dart diff --git a/lib/servicios/servicio_favoritos.dart b/lib/servicios/servicio_favoritos.dart new file mode 100644 index 0000000..c40470e --- /dev/null +++ b/lib/servicios/servicio_favoritos.dart @@ -0,0 +1,251 @@ +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; + +import '../modelos/emisora.dart'; + +/// Servicio de persistencia de emisoras favoritas con SQLite (sqflite). +/// +/// ### Inicialización lazy +/// La base de datos se abre en el primer acceso; no es necesario llamar +/// a ningún método `init()` explícito. +/// +/// ### Migration-ready +/// El esquema está versionado (`_dbVersion`). Para añadir columnas en una +/// versión futura, implementa el caso correspondiente en `_onUpgrade`. +/// La versión actual es **1**. +/// +/// ### Eliminación lógica +/// No se borran filas físicas sin confirmación explícita. El método +/// [eliminar] hace `DELETE` por `uuid` (la tabla de favoritos es propiedad +/// del usuario y el borrado es una acción explícita y reversible vía +/// [agregar]). Si en el futuro se requiere eliminación lógica, añade la +/// columna `eliminado` en la migración a versión 2. +/// +/// ### Uso básico +/// ```dart +/// final servicio = ServicioFavoritos(); +/// +/// await servicio.agregar(emisora); +/// final lista = await servicio.obtenerTodos(); +/// final esFav = await servicio.esFavorito('some-uuid'); +/// await servicio.reordenar('some-uuid', 3); +/// await servicio.eliminar('some-uuid'); +/// ``` +class ServicioFavoritos { + // ─── Constantes de esquema ──────────────────────────────────────────────── + + static const String _dbNombre = 'pluriwave.db'; + static const int _dbVersion = 1; + + static const String _tabla = 'favoritos'; + + // Columnas + static const String _colId = 'id'; + static const String _colUuid = 'uuid'; + static const String _colNombre = 'nombre'; + static const String _colUrl = 'url'; + static const String _colFavicon = 'favicon'; + static const String _colPais = 'pais'; + static const String _colIdioma = 'idioma'; + static const String _colTags = 'tags'; + static const String _colOrden = 'orden'; + + // ─── Estado interno ─────────────────────────────────────────────────────── + + /// Instancia única del servicio (singleton ligero). + static final ServicioFavoritos _instancia = ServicioFavoritos._interno(); + + factory ServicioFavoritos() => _instancia; + + ServicioFavoritos._interno(); + + Database? _db; + + // ─── Acceso a la BD (lazy) ──────────────────────────────────────────────── + + /// Devuelve la instancia abierta de la BD. + /// La abre y crea las tablas en el primer acceso. + Future get _database async { + _db ??= await _abrirBd(); + return _db!; + } + + Future _abrirBd() async { + final ruta = join(await getDatabasesPath(), _dbNombre); + return openDatabase( + ruta, + version: _dbVersion, + onCreate: _onCreate, + onUpgrade: _onUpgrade, + ); + } + + // ─── Callbacks de ciclo de vida de la BD ───────────────────────────────── + + /// Crea el esquema inicial (versión 1). + Future _onCreate(Database db, int version) async { + await db.execute(''' + CREATE TABLE IF NOT EXISTS $_tabla ( + $_colId INTEGER PRIMARY KEY AUTOINCREMENT, + $_colUuid TEXT NOT NULL UNIQUE, + $_colNombre TEXT NOT NULL, + $_colUrl TEXT NOT NULL, + $_colFavicon TEXT, + $_colPais TEXT, + $_colIdioma TEXT, + $_colTags TEXT, + $_colOrden INTEGER NOT NULL DEFAULT 0 + ) + '''); + + // Índice para búsquedas frecuentes por uuid + await db.execute( + 'CREATE UNIQUE INDEX IF NOT EXISTS idx_favoritos_uuid ON $_tabla ($_colUuid)', + ); + + // Índice para ordenación + await db.execute( + 'CREATE INDEX IF NOT EXISTS idx_favoritos_orden ON $_tabla ($_colOrden)', + ); + } + + /// Migraciones futuras. + /// + /// Añade un `case` por cada nueva versión. No uses `DROP COLUMN` — + /// marca la columna como obsoleta y elimínala en el siguiente sprint. + Future _onUpgrade(Database db, int oldVersion, int newVersion) async { + for (var v = oldVersion + 1; v <= newVersion; v++) { + switch (v) { + // Ejemplo para versión 2: + // case 2: + // await db.execute( + // 'ALTER TABLE $_tabla ADD COLUMN nueva_col TEXT', + // ); + default: + break; + } + } + } + + // ─── API pública ────────────────────────────────────────────────────────── + + /// Devuelve todas las emisoras favoritas ordenadas por [_colOrden] ASC. + /// + /// Nunca devuelve null; devuelve lista vacía si no hay favoritos. + Future> obtenerTodos() async { + final db = await _database; + final filas = await db.query( + _tabla, + orderBy: '$_colOrden ASC, $_colId ASC', + ); + return filas.map(Emisora.fromMap).toList(); + } + + /// Agrega [emisora] a la lista de favoritos. + /// + /// Si ya existe una emisora con el mismo [Emisora.uuid], actualiza todos + /// sus campos (upsert). El campo [Emisora.orden] se asigna al final de la + /// lista cuando su valor es 0 y la emisora es nueva. + /// + /// Devuelve el [id] de la fila insertada o actualizada. + Future agregar(Emisora emisora) async { + final db = await _database; + + // Calcular orden automático si viene en 0 y la emisora es nueva + Emisora emisoraFinal = emisora; + if (emisora.orden == 0) { + final existe = await esFavorito(emisora.uuid); + if (!existe) { + final maxOrden = await _maxOrden(db); + emisoraFinal = emisora.copyWith(orden: maxOrden + 1); + } + } + + return db.insert( + _tabla, + emisoraFinal.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + /// Elimina la emisora con [uuid] de la lista de favoritos. + /// + /// Si la emisora no existe, no hace nada (idempotente). + /// Devuelve el número de filas eliminadas (0 ó 1). + Future eliminar(String uuid) async { + final db = await _database; + return db.delete( + _tabla, + where: '$_colUuid = ?', + whereArgs: [uuid], + ); + } + + /// Devuelve `true` si la emisora con [uuid] está en la lista de favoritos. + Future esFavorito(String uuid) async { + final db = await _database; + final resultado = await db.query( + _tabla, + columns: [_colId], + where: '$_colUuid = ?', + whereArgs: [uuid], + limit: 1, + ); + return resultado.isNotEmpty; + } + + /// Actualiza el [orden] de la emisora con [uuid] al valor [nuevoOrden]. + /// + /// Si la emisora no existe, no hace nada (idempotente). + /// Devuelve el número de filas actualizadas (0 ó 1). + /// + /// Nota: este método actualiza solo la columna `orden` de la emisora + /// indicada. Si necesitas reordenar toda la lista de una vez (drag & drop), + /// construye una lista ordenada y llama a [reordenarLista]. + Future reordenar(String uuid, int nuevoOrden) async { + final db = await _database; + return db.update( + _tabla, + {_colOrden: nuevoOrden}, + where: '$_colUuid = ?', + whereArgs: [uuid], + ); + } + + /// Reordena la lista completa de favoritos en una sola transacción. + /// + /// Recibe una lista ordenada de UUIDs y asigna el orden 0, 1, 2... en + /// el mismo orden. Ideal para operaciones de drag & drop. + Future reordenarLista(List uuidsOrdenados) async { + final db = await _database; + await db.transaction((txn) async { + for (var i = 0; i < uuidsOrdenados.length; i++) { + await txn.update( + _tabla, + {_colOrden: i}, + where: '$_colUuid = ?', + whereArgs: [uuidsOrdenados[i]], + ); + } + }); + } + + /// Devuelve el número total de emisoras favoritas. + Future contarFavoritos() async { + final db = await _database; + final resultado = await db.rawQuery( + 'SELECT COUNT(*) AS total FROM $_tabla', + ); + return resultado.first['total'] as int? ?? 0; + } + + // ─── Helpers privados ───────────────────────────────────────────────────── + + /// Devuelve el valor máximo actual de [_colOrden], o 0 si la tabla está vacía. + Future _maxOrden(Database db) async { + final resultado = await db.rawQuery( + 'SELECT MAX($_colOrden) AS max_orden FROM $_tabla', + ); + return resultado.first['max_orden'] as int? ?? 0; + } +}