Compare commits
2 Commits
2fe1d60e23
...
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