feat(favorites): add group persistence foundation
This commit is contained in:
@@ -2,15 +2,17 @@ import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
import '../modelos/emisora.dart';
|
||||
import '../modelos/grupo_favoritos.dart';
|
||||
|
||||
/// Servicio de persistencia de emisoras favoritas con SQLite.
|
||||
///
|
||||
/// - Inicialización lazy: la BD se abre en el primer acceso.
|
||||
/// - Migration-ready: versión 2 añade campos de la Radio Browser API.
|
||||
/// - Inicializaci?n lazy: la BD se abre en el primer acceso.
|
||||
/// - Migration-ready: versi?n 3 a?ade agrupaciones de favoritos.
|
||||
class ServicioFavoritos {
|
||||
static const _dbName = 'pluriwave.db';
|
||||
static const _dbVersion = 2;
|
||||
static const _dbVersion = 3;
|
||||
|
||||
Database? _db;
|
||||
|
||||
@@ -48,9 +50,12 @@ class ServicioFavoritos {
|
||||
bitrate INTEGER,
|
||||
votes INTEGER NOT NULL DEFAULT 0,
|
||||
clickcount INTEGER NOT NULL DEFAULT 0,
|
||||
orden INTEGER NOT NULL DEFAULT 0
|
||||
orden INTEGER NOT NULL DEFAULT 0,
|
||||
grupo_id TEXT NOT NULL DEFAULT 'sin_asignar'
|
||||
)
|
||||
''');
|
||||
await _crearTablaGrupos(db);
|
||||
await _asegurarGrupoSinAsignar(db);
|
||||
}
|
||||
|
||||
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||
@@ -74,8 +79,8 @@ class ServicioFavoritos {
|
||||
}
|
||||
}
|
||||
|
||||
// Migración defensiva: algunas instalaciones antiguas pueden venir de
|
||||
// esquemas intermedios. No asumimos qué columna existe: la verificamos.
|
||||
// Migraci?n defensiva: algunas instalaciones antiguas pueden venir de
|
||||
// esquemas intermedios. No asumimos qu? columna existe: la verificamos.
|
||||
await addColumn('favicon', 'TEXT');
|
||||
await addColumn('pais', 'TEXT');
|
||||
await addColumn('codigo_pais', 'TEXT');
|
||||
@@ -86,6 +91,36 @@ class ServicioFavoritos {
|
||||
await addColumn('votes', 'INTEGER NOT NULL DEFAULT 0');
|
||||
await addColumn('clickcount', 'INTEGER NOT NULL DEFAULT 0');
|
||||
await addColumn('orden', 'INTEGER NOT NULL DEFAULT 0');
|
||||
await addColumn(
|
||||
'grupo_id',
|
||||
"TEXT NOT NULL DEFAULT '${GrupoFavoritos.sinAsignarId}'",
|
||||
);
|
||||
await _crearTablaGrupos(db);
|
||||
await _asegurarGrupoSinAsignar(db);
|
||||
}
|
||||
|
||||
Future<void> _crearTablaGrupos(Database db) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS grupos_favoritos (
|
||||
id TEXT PRIMARY KEY,
|
||||
nombre TEXT NOT NULL,
|
||||
orden INTEGER NOT NULL DEFAULT 0,
|
||||
protegido INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
Future<void> _asegurarGrupoSinAsignar(Database db) async {
|
||||
await db.insert(
|
||||
'grupos_favoritos',
|
||||
const GrupoFavoritos(
|
||||
id: GrupoFavoritos.sinAsignarId,
|
||||
nombre: 'Sin asignar',
|
||||
orden: 0,
|
||||
protegido: true,
|
||||
).toMap(),
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Set<String>> _columnas(Database db, String tabla) async {
|
||||
@@ -93,22 +128,94 @@ class ServicioFavoritos {
|
||||
return info.map((row) => row['name'] as String).toSet();
|
||||
}
|
||||
|
||||
/// Devuelve todas las emisoras favoritas ordenadas por [orden].
|
||||
Future<List<Emisora>> obtenerTodos() async {
|
||||
final db = await _database;
|
||||
final rows = await db.query('favoritos', orderBy: 'orden ASC');
|
||||
return rows.map(Emisora.fromMap).toList();
|
||||
}
|
||||
|
||||
/// Añade una emisora a favoritos. Si ya existe (mismo uuid), la actualiza.
|
||||
Future<List<GrupoFavoritos>> obtenerGrupos() async {
|
||||
final db = await _database;
|
||||
final rows = await db.query(
|
||||
'grupos_favoritos',
|
||||
orderBy: 'orden ASC, nombre ASC',
|
||||
);
|
||||
return rows.map(GrupoFavoritos.fromMap).toList();
|
||||
}
|
||||
|
||||
Future<GrupoFavoritos> crearGrupo(String nombre) async {
|
||||
final db = await _database;
|
||||
final normalizado = _normalizarNombreGrupo(nombre);
|
||||
final maxOrden = Sqflite.firstIntValue(
|
||||
await db.rawQuery('SELECT MAX(orden) FROM grupos_favoritos'),
|
||||
) ??
|
||||
0;
|
||||
final grupo = GrupoFavoritos(
|
||||
id: 'grupo_${DateTime.now().microsecondsSinceEpoch}',
|
||||
nombre: normalizado,
|
||||
orden: maxOrden + 1,
|
||||
);
|
||||
await db.insert('grupos_favoritos', grupo.toMap());
|
||||
return grupo;
|
||||
}
|
||||
|
||||
Future<void> renombrarGrupo(String id, String nombre) async {
|
||||
if (id == GrupoFavoritos.sinAsignarId) return;
|
||||
final db = await _database;
|
||||
await db.update(
|
||||
'grupos_favoritos',
|
||||
{'nombre': _normalizarNombreGrupo(nombre)},
|
||||
where: 'id = ? AND protegido = 0',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> eliminarGrupo(String id) async {
|
||||
if (id == GrupoFavoritos.sinAsignarId) return;
|
||||
final db = await _database;
|
||||
await db.transaction((txn) async {
|
||||
await txn.update(
|
||||
'favoritos',
|
||||
{'grupo_id': GrupoFavoritos.sinAsignarId},
|
||||
where: 'grupo_id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
await txn.delete(
|
||||
'grupos_favoritos',
|
||||
where: 'id = ? AND protegido = 0',
|
||||
whereArgs: [id],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> asignarGrupo(String uuid, String grupoId) async {
|
||||
final db = await _database;
|
||||
final existe = Sqflite.firstIntValue(
|
||||
await db.rawQuery(
|
||||
'SELECT COUNT(*) FROM grupos_favoritos WHERE id = ?',
|
||||
[grupoId],
|
||||
),
|
||||
) ??
|
||||
0;
|
||||
final destino = existe > 0 ? grupoId : GrupoFavoritos.sinAsignarId;
|
||||
await db.update(
|
||||
'favoritos',
|
||||
{'grupo_id': destino},
|
||||
where: 'uuid = ?',
|
||||
whereArgs: [uuid],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> agregar(Emisora emisora) async {
|
||||
final db = await _database;
|
||||
// Calcular el siguiente orden
|
||||
final maxOrden = Sqflite.firstIntValue(
|
||||
await db.rawQuery('SELECT MAX(orden) FROM favoritos'),
|
||||
) ??
|
||||
-1;
|
||||
final nuevaEmisora = emisora.copyWith(orden: maxOrden + 1);
|
||||
final nuevaEmisora = emisora.copyWith(
|
||||
orden: maxOrden + 1,
|
||||
grupoFavoritosId: GrupoFavoritos.sinAsignarId,
|
||||
);
|
||||
await db.insert(
|
||||
'favoritos',
|
||||
nuevaEmisora.toMap(),
|
||||
@@ -116,13 +223,11 @@ class ServicioFavoritos {
|
||||
);
|
||||
}
|
||||
|
||||
/// Elimina una emisora de favoritos por [uuid].
|
||||
Future<void> eliminar(String uuid) async {
|
||||
final db = await _database;
|
||||
await db.delete('favoritos', where: 'uuid = ?', whereArgs: [uuid]);
|
||||
}
|
||||
|
||||
/// Devuelve true si la emisora con [uuid] está en favoritos.
|
||||
Future<bool> esFavorito(String uuid) async {
|
||||
final db = await _database;
|
||||
final count = Sqflite.firstIntValue(
|
||||
@@ -134,7 +239,6 @@ class ServicioFavoritos {
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
|
||||
/// Alterna el estado de favorito de una emisora.
|
||||
Future<bool> toggleFavorito(Emisora emisora) async {
|
||||
if (await esFavorito(emisora.uuid)) {
|
||||
await eliminar(emisora.uuid);
|
||||
@@ -145,7 +249,6 @@ class ServicioFavoritos {
|
||||
}
|
||||
}
|
||||
|
||||
/// Actualiza el orden de un favorito.
|
||||
Future<void> reordenar(String uuid, int nuevoOrden) async {
|
||||
final db = await _database;
|
||||
await db.transaction((txn) async {
|
||||
@@ -172,4 +275,15 @@ class ServicioFavoritos {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String _normalizarNombreGrupo(String nombre) {
|
||||
final normalizado = nombre.trim().replaceAll(RegExp(r'\s+'), ' ');
|
||||
if (normalizado.isEmpty) {
|
||||
throw ArgumentError('El nombre del grupo no puede estar vac?o');
|
||||
}
|
||||
if (normalizado.length > 28) {
|
||||
throw ArgumentError('El nombre del grupo no puede superar 28 caracteres');
|
||||
}
|
||||
return normalizado;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user