Compare commits
2 Commits
feature/ci
...
feature/bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f818cbc43a | ||
|
|
a3444d7c49 |
97
lib/modelos/emisora.dart
Normal file
97
lib/modelos/emisora.dart
Normal file
@@ -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<String, dynamic> 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<String, dynamic> 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;
|
||||||
|
}
|
||||||
251
lib/servicios/servicio_favoritos.dart
Normal file
251
lib/servicios/servicio_favoritos.dart
Normal file
@@ -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<Database> get _database async {
|
||||||
|
_db ??= await _abrirBd();
|
||||||
|
return _db!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Database> _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<void> _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<void> _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<List<Emisora>> 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<int> 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<int> 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<bool> 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<int> 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<void> reordenarLista(List<String> 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<int> 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<int> _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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user