Some checks failed
Flutter CI/CD — PluriWave / Test + Build (pull_request) Has been cancelled
- Modelo Emisora: campos completos Radio Browser API (fromApi + fromMap) - ServicioRadio: cliente Radio Browser API (populares, tendencias, buscar por nombre/país/idioma/tag) - ServicioAudio: just_audio + audio_service wrapper (play/pause/stop/toggle, fade, background handler) - ServicioTimer: countdown con fade out gradual (15/30/60/90 min) - ServicioFavoritos: actualizado a v2 con campos codec/bitrate/votes/clickcount - EstadoRadio: ChangeNotifier global con Provider - PantallaInicio: grid emisoras populares, chips género, shimmer loading, pull-to-refresh - PantallaBuscar: SearchBar + filtros país/idioma, lista resultados - PantallaFavoritos: ReorderableListView + swipe-to-delete (Dismissible) - TarjetaEmisora: card + modo compacto ListTile, cached_network_image, shimmer fallback - MiniReproductor: barra inferior persistente con stream de estado - app.dart: MaterialApp + Provider + NavigationBar + timer dialog - main.dart: punto de entrada limpio - AndroidManifest.xml: permisos INTERNET + FOREGROUND_SERVICE + audio_service receivers
150 lines
4.1 KiB
Dart
150 lines
4.1 KiB
Dart
/// Modelo de datos de una emisora de radio.
|
|
///
|
|
/// Unifica los campos de la Radio Browser API con los de la tabla SQLite
|
|
/// de favoritos. Los campos opcionales 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? codigoPais; // ISO 3166-1 alpha-2
|
|
final String? idioma;
|
|
final String? tags;
|
|
final String? codec;
|
|
final int? bitrate;
|
|
final int votes;
|
|
final int clickcount;
|
|
final int orden;
|
|
|
|
const Emisora({
|
|
this.id,
|
|
required this.uuid,
|
|
required this.nombre,
|
|
required this.url,
|
|
this.favicon,
|
|
this.pais,
|
|
this.codigoPais,
|
|
this.idioma,
|
|
this.tags,
|
|
this.codec,
|
|
this.bitrate,
|
|
this.votes = 0,
|
|
this.clickcount = 0,
|
|
this.orden = 0,
|
|
});
|
|
|
|
/// Construye una [Emisora] desde la respuesta JSON de Radio Browser API.
|
|
factory Emisora.fromApi(Map<String, dynamic> json) {
|
|
return Emisora(
|
|
uuid: json['stationuuid'] as String? ?? '',
|
|
nombre: json['name'] as String? ?? 'Sin nombre',
|
|
url: json['url_resolved'] as String? ?? json['url'] as String? ?? '',
|
|
favicon: _nonEmpty(json['favicon'] as String?),
|
|
pais: _nonEmpty(json['country'] as String?),
|
|
codigoPais: _nonEmpty(json['countrycode'] as String?),
|
|
idioma: _nonEmpty(json['language'] as String?),
|
|
tags: _nonEmpty(json['tags'] as String?),
|
|
codec: _nonEmpty(json['codec'] as String?),
|
|
bitrate: json['bitrate'] as int?,
|
|
votes: json['votes'] as int? ?? 0,
|
|
clickcount: json['clickcount'] as int? ?? 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?,
|
|
codigoPais: map['codigo_pais'] as String?,
|
|
idioma: map['idioma'] as String?,
|
|
tags: map['tags'] as String?,
|
|
codec: map['codec'] as String?,
|
|
bitrate: map['bitrate'] as int?,
|
|
votes: map['votes'] as int? ?? 0,
|
|
clickcount: map['clickcount'] as int? ?? 0,
|
|
orden: map['orden'] as int? ?? 0,
|
|
);
|
|
}
|
|
|
|
/// Serializa para inserción/actualización en SQLite.
|
|
Map<String, dynamic> toMap() {
|
|
return {
|
|
'uuid': uuid,
|
|
'nombre': nombre,
|
|
'url': url,
|
|
'favicon': favicon,
|
|
'pais': pais,
|
|
'codigo_pais': codigoPais,
|
|
'idioma': idioma,
|
|
'tags': tags,
|
|
'codec': codec,
|
|
'bitrate': bitrate,
|
|
'votes': votes,
|
|
'clickcount': clickcount,
|
|
'orden': orden,
|
|
};
|
|
}
|
|
|
|
Emisora copyWith({
|
|
int? id,
|
|
String? uuid,
|
|
String? nombre,
|
|
String? url,
|
|
String? favicon,
|
|
String? pais,
|
|
String? codigoPais,
|
|
String? idioma,
|
|
String? tags,
|
|
String? codec,
|
|
int? bitrate,
|
|
int? votes,
|
|
int? clickcount,
|
|
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,
|
|
codigoPais: codigoPais ?? this.codigoPais,
|
|
idioma: idioma ?? this.idioma,
|
|
tags: tags ?? this.tags,
|
|
codec: codec ?? this.codec,
|
|
bitrate: bitrate ?? this.bitrate,
|
|
votes: votes ?? this.votes,
|
|
clickcount: clickcount ?? this.clickcount,
|
|
orden: orden ?? this.orden,
|
|
);
|
|
}
|
|
|
|
/// Lista de géneros/tags como lista limpia.
|
|
List<String> get generos {
|
|
if (tags == null || tags!.isEmpty) return [];
|
|
return tags!.split(',').map((t) => t.trim()).where((t) => t.isNotEmpty).toList();
|
|
}
|
|
|
|
static String? _nonEmpty(String? s) =>
|
|
(s == null || s.trim().isEmpty) ? null : s.trim();
|
|
|
|
@override
|
|
String toString() => 'Emisora(uuid: $uuid, nombre: $nombre)';
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is Emisora && runtimeType == other.runtimeType && uuid == other.uuid;
|
|
|
|
@override
|
|
int get hashCode => uuid.hashCode;
|
|
}
|