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; } }