feat(favorites): add group persistence foundation
This commit is contained in:
@@ -43,5 +43,7 @@
|
|||||||
- [ ] Permitir crear listas de favoritos con nombre corto configurable por el usuario desde Ajustes.
|
- [ ] Permitir crear listas de favoritos con nombre corto configurable por el usuario desde Ajustes.
|
||||||
- [ ] Mantener siempre un grupo interno por defecto traducible llamado "Sin asignar", no editable y no borrable.
|
- [ ] Mantener siempre un grupo interno por defecto traducible llamado "Sin asignar", no editable y no borrable.
|
||||||
- [ ] Gestionar desde la vista Favoritos qu? emisoras pertenecen a cada agrupaci?n/lista.
|
- [ ] Gestionar desde la vista Favoritos qu? emisoras pertenecen a cada agrupaci?n/lista.
|
||||||
- [ ] Dise?ar migraci?n SQLite para asociar favoritos existentes al grupo "Sin asignar" sin perder datos.
|
- [x] Dise?ar migraci?n SQLite base para asociar favoritos existentes al grupo "Sin asignar" sin perder datos.
|
||||||
|
- [ ] Completar UI en Ajustes para crear, editar y borrar listas de favoritos.
|
||||||
|
- [ ] Completar UI en Favoritos para mover emisoras entre listas.
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../modelos/emisora.dart';
|
import '../modelos/emisora.dart';
|
||||||
|
import '../modelos/grupo_favoritos.dart';
|
||||||
import '../modelos/preset_ecualizador.dart';
|
import '../modelos/preset_ecualizador.dart';
|
||||||
import '../servicios/servicio_audio.dart';
|
import '../servicios/servicio_audio.dart';
|
||||||
import '../servicios/servicio_ecualizador.dart';
|
import '../servicios/servicio_ecualizador.dart';
|
||||||
@@ -68,6 +69,7 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
List<Emisora> _resultadosBusqueda = [];
|
List<Emisora> _resultadosBusqueda = [];
|
||||||
List<Emisora> _emisorasCercanas = [];
|
List<Emisora> _emisorasCercanas = [];
|
||||||
List<Emisora> _listaFavoritos = [];
|
List<Emisora> _listaFavoritos = [];
|
||||||
|
List<GrupoFavoritos> _gruposFavoritos = [];
|
||||||
List<Emisora> _emisorasCustom = [];
|
List<Emisora> _emisorasCustom = [];
|
||||||
|
|
||||||
// Presets EQ guardados por uuid de emisora.
|
// Presets EQ guardados por uuid de emisora.
|
||||||
@@ -114,6 +116,7 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
List<Emisora> get resultadosBusqueda => _ordenarEmisoras(_resultadosBusqueda);
|
List<Emisora> get resultadosBusqueda => _ordenarEmisoras(_resultadosBusqueda);
|
||||||
List<Emisora> get emisorasCercanas => _ordenarEmisoras(_emisorasCercanas);
|
List<Emisora> get emisorasCercanas => _ordenarEmisoras(_emisorasCercanas);
|
||||||
List<Emisora> get listaFavoritos => _ordenarEmisoras(_listaFavoritos);
|
List<Emisora> get listaFavoritos => _ordenarEmisoras(_listaFavoritos);
|
||||||
|
List<GrupoFavoritos> get gruposFavoritos => List.unmodifiable(_gruposFavoritos);
|
||||||
List<Emisora> get emisorasCustom => _ordenarEmisoras(_emisorasCustom);
|
List<Emisora> get emisorasCustom => _ordenarEmisoras(_emisorasCustom);
|
||||||
bool get cargandoPopulares => _cargandoPopulares;
|
bool get cargandoPopulares => _cargandoPopulares;
|
||||||
bool get cargandoBusqueda => _cargandoBusqueda;
|
bool get cargandoBusqueda => _cargandoBusqueda;
|
||||||
@@ -202,6 +205,7 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
await Future.wait([
|
await Future.wait([
|
||||||
cargarPopulares(),
|
cargarPopulares(),
|
||||||
cargarFavoritos(),
|
cargarFavoritos(),
|
||||||
|
cargarGruposFavoritos(),
|
||||||
_cargarEmisorasCustom(),
|
_cargarEmisorasCustom(),
|
||||||
]);
|
]);
|
||||||
await _normalizarEmisoraPreferida();
|
await _normalizarEmisoraPreferida();
|
||||||
@@ -277,6 +281,31 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> cargarGruposFavoritos() async {
|
||||||
|
_gruposFavoritos = await favoritos.obtenerGrupos();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> crearGrupoFavoritos(String nombre) async {
|
||||||
|
await favoritos.crearGrupo(nombre);
|
||||||
|
await cargarGruposFavoritos();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> renombrarGrupoFavoritos(String id, String nombre) async {
|
||||||
|
await favoritos.renombrarGrupo(id, nombre);
|
||||||
|
await cargarGruposFavoritos();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> eliminarGrupoFavoritos(String id) async {
|
||||||
|
await favoritos.eliminarGrupo(id);
|
||||||
|
await Future.wait([cargarFavoritos(), cargarGruposFavoritos()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> asignarGrupoFavorito(String uuid, String grupoId) async {
|
||||||
|
await favoritos.asignarGrupo(uuid, grupoId);
|
||||||
|
await cargarFavoritos();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> cambiarEmisoraPreferida(Emisora? emisora) async {
|
Future<void> cambiarEmisoraPreferida(Emisora? emisora) async {
|
||||||
_emisoraPreferidaUuid = emisora?.uuid;
|
_emisoraPreferidaUuid = emisora?.uuid;
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class Emisora {
|
|||||||
final int votes;
|
final int votes;
|
||||||
final int clickcount;
|
final int clickcount;
|
||||||
final int orden;
|
final int orden;
|
||||||
|
final String grupoFavoritosId;
|
||||||
|
|
||||||
const Emisora({
|
const Emisora({
|
||||||
this.id,
|
this.id,
|
||||||
@@ -34,6 +35,7 @@ class Emisora {
|
|||||||
this.votes = 0,
|
this.votes = 0,
|
||||||
this.clickcount = 0,
|
this.clickcount = 0,
|
||||||
this.orden = 0,
|
this.orden = 0,
|
||||||
|
this.grupoFavoritosId = 'sin_asignar',
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Construye una [Emisora] desde la respuesta JSON de Radio Browser API.
|
/// Construye una [Emisora] desde la respuesta JSON de Radio Browser API.
|
||||||
@@ -71,6 +73,7 @@ class Emisora {
|
|||||||
votes: map['votes'] as int? ?? 0,
|
votes: map['votes'] as int? ?? 0,
|
||||||
clickcount: map['clickcount'] as int? ?? 0,
|
clickcount: map['clickcount'] as int? ?? 0,
|
||||||
orden: map['orden'] as int? ?? 0,
|
orden: map['orden'] as int? ?? 0,
|
||||||
|
grupoFavoritosId: map['grupo_id'] as String? ?? 'sin_asignar',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +93,7 @@ class Emisora {
|
|||||||
'votes': votes,
|
'votes': votes,
|
||||||
'clickcount': clickcount,
|
'clickcount': clickcount,
|
||||||
'orden': orden,
|
'orden': orden,
|
||||||
|
'grupo_id': grupoFavoritosId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +112,7 @@ class Emisora {
|
|||||||
int? votes,
|
int? votes,
|
||||||
int? clickcount,
|
int? clickcount,
|
||||||
int? orden,
|
int? orden,
|
||||||
|
String? grupoFavoritosId,
|
||||||
}) {
|
}) {
|
||||||
return Emisora(
|
return Emisora(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -124,6 +129,7 @@ class Emisora {
|
|||||||
votes: votes ?? this.votes,
|
votes: votes ?? this.votes,
|
||||||
clickcount: clickcount ?? this.clickcount,
|
clickcount: clickcount ?? this.clickcount,
|
||||||
orden: orden ?? this.orden,
|
orden: orden ?? this.orden,
|
||||||
|
grupoFavoritosId: grupoFavoritosId ?? this.grupoFavoritosId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
class GrupoFavoritos {
|
||||||
|
const GrupoFavoritos({
|
||||||
|
required this.id,
|
||||||
|
required this.nombre,
|
||||||
|
required this.orden,
|
||||||
|
this.protegido = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const sinAsignarId = 'sin_asignar';
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String nombre;
|
||||||
|
final int orden;
|
||||||
|
final bool protegido;
|
||||||
|
|
||||||
|
bool get esSinAsignar => id == sinAsignarId;
|
||||||
|
|
||||||
|
factory GrupoFavoritos.fromMap(Map<String, dynamic> map) {
|
||||||
|
return GrupoFavoritos(
|
||||||
|
id: map['id'] as String,
|
||||||
|
nombre: map['nombre'] as String,
|
||||||
|
orden: map['orden'] as int? ?? 0,
|
||||||
|
protegido: (map['protegido'] as int? ?? 0) == 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'nombre': nombre,
|
||||||
|
'orden': orden,
|
||||||
|
'protegido': protegido ? 1 : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
GrupoFavoritos copyWith({
|
||||||
|
String? id,
|
||||||
|
String? nombre,
|
||||||
|
int? orden,
|
||||||
|
bool? protegido,
|
||||||
|
}) {
|
||||||
|
return GrupoFavoritos(
|
||||||
|
id: id ?? this.id,
|
||||||
|
nombre: nombre ?? this.nombre,
|
||||||
|
orden: orden ?? this.orden,
|
||||||
|
protegido: protegido ?? this.protegido,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,17 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
import '../modelos/emisora.dart';
|
import '../modelos/emisora.dart';
|
||||||
|
import '../modelos/grupo_favoritos.dart';
|
||||||
|
|
||||||
/// Servicio de persistencia de emisoras favoritas con SQLite.
|
/// Servicio de persistencia de emisoras favoritas con SQLite.
|
||||||
///
|
///
|
||||||
/// - Inicialización lazy: la BD se abre en el primer acceso.
|
/// - Inicializaci?n lazy: la BD se abre en el primer acceso.
|
||||||
/// - Migration-ready: versión 2 añade campos de la Radio Browser API.
|
/// - Migration-ready: versi?n 3 a?ade agrupaciones de favoritos.
|
||||||
class ServicioFavoritos {
|
class ServicioFavoritos {
|
||||||
static const _dbName = 'pluriwave.db';
|
static const _dbName = 'pluriwave.db';
|
||||||
static const _dbVersion = 2;
|
static const _dbVersion = 3;
|
||||||
|
|
||||||
Database? _db;
|
Database? _db;
|
||||||
|
|
||||||
@@ -48,9 +50,12 @@ class ServicioFavoritos {
|
|||||||
bitrate INTEGER,
|
bitrate INTEGER,
|
||||||
votes INTEGER NOT NULL DEFAULT 0,
|
votes INTEGER NOT NULL DEFAULT 0,
|
||||||
clickcount 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 {
|
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
|
// Migraci?n defensiva: algunas instalaciones antiguas pueden venir de
|
||||||
// esquemas intermedios. No asumimos qué columna existe: la verificamos.
|
// esquemas intermedios. No asumimos qu? columna existe: la verificamos.
|
||||||
await addColumn('favicon', 'TEXT');
|
await addColumn('favicon', 'TEXT');
|
||||||
await addColumn('pais', 'TEXT');
|
await addColumn('pais', 'TEXT');
|
||||||
await addColumn('codigo_pais', 'TEXT');
|
await addColumn('codigo_pais', 'TEXT');
|
||||||
@@ -86,6 +91,36 @@ class ServicioFavoritos {
|
|||||||
await addColumn('votes', 'INTEGER NOT NULL DEFAULT 0');
|
await addColumn('votes', 'INTEGER NOT NULL DEFAULT 0');
|
||||||
await addColumn('clickcount', 'INTEGER NOT NULL DEFAULT 0');
|
await addColumn('clickcount', 'INTEGER NOT NULL DEFAULT 0');
|
||||||
await addColumn('orden', '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 {
|
Future<Set<String>> _columnas(Database db, String tabla) async {
|
||||||
@@ -93,22 +128,94 @@ class ServicioFavoritos {
|
|||||||
return info.map((row) => row['name'] as String).toSet();
|
return info.map((row) => row['name'] as String).toSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve todas las emisoras favoritas ordenadas por [orden].
|
|
||||||
Future<List<Emisora>> obtenerTodos() async {
|
Future<List<Emisora>> obtenerTodos() async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final rows = await db.query('favoritos', orderBy: 'orden ASC');
|
final rows = await db.query('favoritos', orderBy: 'orden ASC');
|
||||||
return rows.map(Emisora.fromMap).toList();
|
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 {
|
Future<void> agregar(Emisora emisora) async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
// Calcular el siguiente orden
|
|
||||||
final maxOrden = Sqflite.firstIntValue(
|
final maxOrden = Sqflite.firstIntValue(
|
||||||
await db.rawQuery('SELECT MAX(orden) FROM favoritos'),
|
await db.rawQuery('SELECT MAX(orden) FROM favoritos'),
|
||||||
) ??
|
) ??
|
||||||
-1;
|
-1;
|
||||||
final nuevaEmisora = emisora.copyWith(orden: maxOrden + 1);
|
final nuevaEmisora = emisora.copyWith(
|
||||||
|
orden: maxOrden + 1,
|
||||||
|
grupoFavoritosId: GrupoFavoritos.sinAsignarId,
|
||||||
|
);
|
||||||
await db.insert(
|
await db.insert(
|
||||||
'favoritos',
|
'favoritos',
|
||||||
nuevaEmisora.toMap(),
|
nuevaEmisora.toMap(),
|
||||||
@@ -116,13 +223,11 @@ class ServicioFavoritos {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina una emisora de favoritos por [uuid].
|
|
||||||
Future<void> eliminar(String uuid) async {
|
Future<void> eliminar(String uuid) async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
await db.delete('favoritos', where: 'uuid = ?', whereArgs: [uuid]);
|
await db.delete('favoritos', where: 'uuid = ?', whereArgs: [uuid]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve true si la emisora con [uuid] está en favoritos.
|
|
||||||
Future<bool> esFavorito(String uuid) async {
|
Future<bool> esFavorito(String uuid) async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final count = Sqflite.firstIntValue(
|
final count = Sqflite.firstIntValue(
|
||||||
@@ -134,7 +239,6 @@ class ServicioFavoritos {
|
|||||||
return (count ?? 0) > 0;
|
return (count ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alterna el estado de favorito de una emisora.
|
|
||||||
Future<bool> toggleFavorito(Emisora emisora) async {
|
Future<bool> toggleFavorito(Emisora emisora) async {
|
||||||
if (await esFavorito(emisora.uuid)) {
|
if (await esFavorito(emisora.uuid)) {
|
||||||
await eliminar(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 {
|
Future<void> reordenar(String uuid, int nuevoOrden) async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
await db.transaction((txn) async {
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -367,6 +367,38 @@ void main() {
|
|||||||
await estado.toggleFavorito(emisora);
|
await estado.toggleFavorito(emisora);
|
||||||
expect(estado.listaFavoritos.any((e) => e.uuid == emisora.uuid), isFalse);
|
expect(estado.listaFavoritos.any((e) => e.uuid == emisora.uuid), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('crea y asigna grupos de favoritos sin tocar Sin asignar', () async {
|
||||||
|
final favoritos = FakeServicioFavoritos();
|
||||||
|
final emisora = emisoraDemo(uuid: 'fav-group', nombre: 'Grupo');
|
||||||
|
await favoritos.agregar(emisora);
|
||||||
|
final estado = EstadoRadio(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
favoritos: favoritos,
|
||||||
|
radio: FakeServicioRadio(populares: [emisora]),
|
||||||
|
servicioEcualizador: FakeServicioEcualizador(),
|
||||||
|
resolverArchivoCustom: _archivoCustomVacio,
|
||||||
|
iniciarAutomaticamente: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await estado.inicializar();
|
||||||
|
expect(estado.gruposFavoritos.first.protegido, isTrue);
|
||||||
|
|
||||||
|
await estado.crearGrupoFavoritos('Noticias');
|
||||||
|
final grupo = estado.gruposFavoritos.last;
|
||||||
|
await estado.asignarGrupoFavorito(emisora.uuid, grupo.id);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
estado.listaFavoritos.first.grupoFavoritosId,
|
||||||
|
grupo.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
await estado.eliminarGrupoFavoritos(grupo.id);
|
||||||
|
expect(
|
||||||
|
estado.listaFavoritos.first.grupoFavoritosId,
|
||||||
|
'sin_asignar',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:pluriwave/modelos/emisora.dart';
|
import 'package:pluriwave/modelos/emisora.dart';
|
||||||
|
import 'package:pluriwave/modelos/grupo_favoritos.dart';
|
||||||
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||||
import 'package:pluriwave/servicios/servicio_audio.dart';
|
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||||
import 'package:pluriwave/servicios/servicio_ecualizador.dart';
|
import 'package:pluriwave/servicios/servicio_ecualizador.dart';
|
||||||
@@ -71,6 +72,14 @@ class FakeServicioAudio extends ServicioAudio {
|
|||||||
|
|
||||||
class FakeServicioFavoritos extends ServicioFavoritos {
|
class FakeServicioFavoritos extends ServicioFavoritos {
|
||||||
final List<Emisora> _favoritos = [];
|
final List<Emisora> _favoritos = [];
|
||||||
|
final List<GrupoFavoritos> _grupos = [
|
||||||
|
const GrupoFavoritos(
|
||||||
|
id: GrupoFavoritos.sinAsignarId,
|
||||||
|
nombre: 'Sin asignar',
|
||||||
|
orden: 0,
|
||||||
|
protegido: true,
|
||||||
|
),
|
||||||
|
];
|
||||||
int toggleCalls = 0;
|
int toggleCalls = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -106,6 +115,51 @@ class FakeServicioFavoritos extends ServicioFavoritos {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<GrupoFavoritos>> obtenerGrupos() async => List.unmodifiable(_grupos);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<GrupoFavoritos> crearGrupo(String nombre) async {
|
||||||
|
final grupo = GrupoFavoritos(
|
||||||
|
id: 'grupo_${_grupos.length}',
|
||||||
|
nombre: nombre.trim(),
|
||||||
|
orden: _grupos.length,
|
||||||
|
);
|
||||||
|
_grupos.add(grupo);
|
||||||
|
return grupo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> renombrarGrupo(String id, String nombre) async {
|
||||||
|
final index = _grupos.indexWhere((g) => g.id == id && !g.protegido);
|
||||||
|
if (index == -1) return;
|
||||||
|
_grupos[index] = _grupos[index].copyWith(nombre: nombre.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> eliminarGrupo(String id) async {
|
||||||
|
if (id == GrupoFavoritos.sinAsignarId) return;
|
||||||
|
_grupos.removeWhere((g) => g.id == id && !g.protegido);
|
||||||
|
for (var i = 0; i < _favoritos.length; i++) {
|
||||||
|
if (_favoritos[i].grupoFavoritosId == id) {
|
||||||
|
_favoritos[i] = _favoritos[i].copyWith(
|
||||||
|
grupoFavoritosId: GrupoFavoritos.sinAsignarId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> asignarGrupo(String uuid, String grupoId) async {
|
||||||
|
final destino = _grupos.any((g) => g.id == grupoId)
|
||||||
|
? grupoId
|
||||||
|
: GrupoFavoritos.sinAsignarId;
|
||||||
|
final index = _favoritos.indexWhere((e) => e.uuid == uuid);
|
||||||
|
if (index != -1) {
|
||||||
|
_favoritos[index] = _favoritos[index].copyWith(grupoFavoritosId: destino);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> reordenar(String uuid, int nuevoOrden) async {
|
Future<void> reordenar(String uuid, int nuevoOrden) async {
|
||||||
final oldIndex = _favoritos.indexWhere((e) => e.uuid == uuid);
|
final oldIndex = _favoritos.indexWhere((e) => e.uuid == uuid);
|
||||||
|
|||||||
Reference in New Issue
Block a user