fix(player): stabilize first playback and refresh design
Build & Deploy Pluriwave / Análisis de código (push) Successful in 12s
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 1m20s

This commit is contained in:
2026-05-20 22:50:39 +02:00
parent 22e19d1cb0
commit b9cf42b91c
27 changed files with 131 additions and 31 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

@@ -0,0 +1,5 @@
# PluriWave Night Ocean asset sheet prompt
Generated with built-in image_gen for the Night Ocean Broadcast redesign.
Contents: app mark, station fallback artworks, aurora/waveform banner, and navigation glyph assets using teal/amber/cream over navy with no purple/magenta dominance.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 565 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 KiB

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 484 KiB

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

@@ -0,0 +1,5 @@
# PluriWave Night Ocean mockup prompt
Generated with built-in image_gen after user feedback rejecting purple-heavy futuristic UI.
Direction: Night Ocean Broadcast ? midnight navy and petrol teal base, mint action states, warm amber live/accent, cream text/surfaces, practical radio streaming UX with immediate Now Playing flow.
+17 -5
View File
@@ -61,6 +61,7 @@ class EstadoRadio extends ChangeNotifier {
bool _cargandoPopulares = false;
bool _cargandoBusqueda = false;
EstadoReproduccion _estadoReproduccion = EstadoReproduccion.detenido;
String? _errorCarga;
List<Emisora> get populares => _populares;
@@ -118,12 +119,11 @@ class EstadoRadio extends ChangeNotifier {
/// Escucha el stream de estado del audio y gestiona errores de reproducción.
void _escucharErroresReproduccion() {
_suscripcionEstadoAudio = audio.estadoStream.listen((estado) {
if (estado == EstadoReproduccion.error) {
if (timer.activo) {
unawaited(timer.cancelar());
}
notifyListeners();
_estadoReproduccion = estado;
if (estado == EstadoReproduccion.error && timer.activo) {
unawaited(timer.cancelar());
}
notifyListeners();
});
}
@@ -192,7 +192,19 @@ class EstadoRadio extends ChangeNotifier {
}
Future<void> reproducir(Emisora emisora) async {
final actual = audio.emisoraActual;
final mismaEmisoraActiva = actual?.uuid == emisora.uuid;
final yaEstaConectandoOSonando =
_estadoReproduccion == EstadoReproduccion.cargando ||
_estadoReproduccion == EstadoReproduccion.reproduciendo ||
audio.estaSonando;
if (mismaEmisoraActiva && yaEstaConectandoOSonando) {
notifyListeners();
return;
}
try {
notifyListeners();
await audio.reproducir(emisora);
unawaited(radio.registrarClick(emisora.uuid));
await _aplicarPresetActivo(_presetParaEmisora(emisora.uuid));
+3 -1
View File
@@ -8,6 +8,8 @@ import '../widgets/pluri_icon.dart';
import '../widgets/pluri_premium_widgets.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
import 'reproducir_y_abrir.dart';
const _paises = [
('Espana', 'ES'),
('USA', 'US'),
@@ -200,7 +202,7 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
itemBuilder: (context, i) => TarjetaEmisora(
emisora: resultados[i],
esCompacta: true,
onTap: () => context.read<EstadoRadio>().reproducir(resultados[i]),
onTap: () => reproducirYAbrir(context, resultados[i]),
).animate().fadeIn(delay: (i * 20).ms).slideY(begin: 0.08),
);
}
+3 -1
View File
@@ -7,6 +7,8 @@ import '../widgets/pluri_icon.dart';
import '../widgets/pluri_premium_widgets.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
import 'reproducir_y_abrir.dart';
class PantallaFavoritos extends StatelessWidget {
const PantallaFavoritos({super.key});
@@ -85,7 +87,7 @@ class PantallaFavoritos extends StatelessWidget {
key: Key(emisora.uuid),
emisora: emisora,
esCompacta: true,
onTap: () => estado.reproducir(emisora),
onTap: () => reproducirYAbrir(context, emisora),
),
),
IconButton.filledTonal(
+4 -2
View File
@@ -10,6 +10,8 @@ import '../widgets/pluri_icon.dart';
import '../widgets/pluri_premium_widgets.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
import 'reproducir_y_abrir.dart';
/// Pantalla principal: emisoras populares y por género.
class PantallaInicio extends StatefulWidget {
const PantallaInicio({super.key});
@@ -118,7 +120,7 @@ class _PantallaInicioState extends State<PantallaInicio> {
),
label: Text(e.nombre, maxLines: 1),
onPressed:
() => context.read<EstadoRadio>().reproducir(e),
() => reproducirYAbrir(context, e),
).animate().fadeIn(delay: (i * 50).ms);
},
),
@@ -225,7 +227,7 @@ class _PantallaInicioState extends State<PantallaInicio> {
delegate: SliverChildBuilderDelegate(
(context, i) => TarjetaEmisora(
emisora: emisoras[i],
onTap: () => context.read<EstadoRadio>().reproducir(emisoras[i]),
onTap: () => reproducirYAbrir(context, emisoras[i]),
).animate().fadeIn(delay: (i * 30).ms).slideY(begin: 0.1),
childCount: emisoras.length,
),
+15
View File
@@ -0,0 +1,15 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../estado/estado_radio.dart';
import '../modelos/emisora.dart';
import 'pantalla_reproductor.dart';
Future<void> reproducirYAbrir(BuildContext context, Emisora emisora) async {
final estado = context.read<EstadoRadio>();
unawaited(estado.reproducir(emisora));
if (!context.mounted) return;
await PantallaReproductor.abrir(context, emisora);
}
+5 -1
View File
@@ -215,11 +215,15 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
@override
Future<void> playMediaItem(MediaItem mediaItem) async {
this.mediaItem.add(mediaItem);
emisoraActual = _emisoraDesdeMediaItem(mediaItem);
playbackState.add(playbackState.value.copyWith(
processingState: AudioProcessingState.loading,
playing: false,
));
try {
await _player.stop();
await _player.setUrl(mediaItem.id);
await _player.play();
emisoraActual = _emisoraDesdeMediaItem(mediaItem);
await _activarEcualizador();
} on PlayerException catch (e) {
_gestionarErrorReproduccion(e);
+6 -6
View File
@@ -9,12 +9,12 @@ abstract final class PluriWaveTheme {
const tokens = PluriWaveTokens.dark;
final colorScheme = const ColorScheme.dark().copyWith(
primary: tokens.electricMagenta,
secondary: const Color(0xFF20E6FF),
secondary: const Color(0xFF7EE4C2),
tertiary: tokens.warmCoral,
surface: const Color(0xFF0B1024),
surfaceContainerLow: const Color(0xFF111831),
surfaceContainerHighest: const Color(0xFF202946),
onSurface: const Color(0xFFF7F2FF),
surface: const Color(0xFF0D1B24),
surfaceContainerLow: const Color(0xFF102532),
surfaceContainerHighest: const Color(0xFF1B3942),
onSurface: const Color(0xFFF2F7FA),
onPrimary: Colors.white,
);
@@ -30,7 +30,7 @@ abstract final class PluriWaveTheme {
appBarTheme: const AppBarTheme(
centerTitle: false,
backgroundColor: Colors.transparent,
foregroundColor: Color(0xFFF7F2FF),
foregroundColor: Color(0xFFF2F7FA),
elevation: 0,
scrolledUnderElevation: 0,
),
+6 -6
View File
@@ -37,12 +37,12 @@ class PluriWaveTokens extends ThemeExtension<PluriWaveTokens> {
final double spacingLg;
static const dark = PluriWaveTokens(
deepViolet: Color(0xFF070A18),
electricMagenta: Color(0xFFFF3DF2),
warmCoral: Color(0xFFFFB86B),
glassSurface: Color(0x24FFFFFF),
glassBorder: Color(0x52FFFFFF),
glowColor: Color(0x88FF3DF2),
deepViolet: Color(0xFF07121A),
electricMagenta: Color(0xFF21D4D9),
warmCoral: Color(0xFFF4B860),
glassSurface: Color(0x1FFFFFFF),
glassBorder: Color(0x33FFFFFF),
glowColor: Color(0x6621D4D9),
radiusSm: 14,
radiusMd: 22,
radiusLg: 30,
+7 -7
View File
@@ -31,10 +31,10 @@ class PluriWaveScaffold extends StatelessWidget {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF070A18),
t.deepViolet,
const Color(0xFF151033),
const Color(0xFF070A18),
const Color(0xFF07121A),
const Color(0xFF0D1B24),
const Color(0xFF0E4A4F),
const Color(0xFF07121A),
],
stops: const [0, 0.34, 0.68, 1],
),
@@ -45,17 +45,17 @@ class PluriWaveScaffold extends StatelessWidget {
Positioned(
left: -120,
top: -120,
child: _AuroraOrb(size: 300, color: const Color(0xFF20E6FF).withValues(alpha: 0.32)),
child: _AuroraOrb(size: 300, color: const Color(0xFF21D4D9).withValues(alpha: 0.18)),
),
Positioned(
right: -150,
top: 160,
child: _AuroraOrb(size: 340, color: t.electricMagenta.withValues(alpha: 0.26)),
child: _AuroraOrb(size: 340, color: const Color(0xFF7EE4C2).withValues(alpha: 0.12)),
),
Positioned(
left: -90,
bottom: 80,
child: _AuroraOrb(size: 260, color: t.warmCoral.withValues(alpha: 0.16)),
child: _AuroraOrb(size: 260, color: t.warmCoral.withValues(alpha: 0.10)),
),
body,
],
+44
View File
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/estado/estado_radio.dart';
import 'package:pluriwave/modelos/preset_ecualizador.dart';
import 'package:pluriwave/servicios/servicio_audio.dart';
import '../helpers/fakes.dart';
@@ -172,6 +173,49 @@ void main() {
expect(audio.presetsAplicados.last, principal);
});
test('notifica cambios de estado de audio para mostrar reproductor al primer play', () async {
final audio = FakeServicioAudio();
final emisora = emisoraDemo(uuid: 'play-1', nombre: 'Primera');
final estado = EstadoRadio(
audio: audio,
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(populares: [emisora]),
servicioEcualizador: FakeServicioEcualizador(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
var notificaciones = 0;
estado.addListener(() => notificaciones++);
await estado.inicializar();
final antes = notificaciones;
audio.emitirEstado(EstadoReproduccion.cargando);
await Future<void>.delayed(Duration.zero);
expect(notificaciones, greaterThan(antes));
});
test('reproducir la misma emisora mientras suena no reinicia el stream', () async {
final audio = FakeServicioAudio();
final emisora = emisoraDemo(uuid: 'same-1', nombre: 'Misma');
final estado = EstadoRadio(
audio: audio,
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(populares: [emisora]),
servicioEcualizador: FakeServicioEcualizador(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
await estado.inicializar();
await estado.reproducir(emisora);
await estado.reproducir(emisora);
expect(audio.emisorasReproducidas, hasLength(1));
});
test('reordenar favoritos reindexa de forma determinística', () async {
final favoritos = FakeServicioFavoritos();
await favoritos.agregar(emisoraDemo(uuid: 'a', nombre: 'A'));
+11 -2
View File
@@ -17,6 +17,7 @@ class FakeServicioAudio extends ServicioAudio {
final List<PresetEcualizador> presetsAplicados = [];
final List<Emisora> emisorasReproducidas = [];
Emisora? _emisoraActual;
EstadoReproduccion _estadoActual = EstadoReproduccion.detenido;
@override
Emisora? get emisoraActual => _emisoraActual;
@@ -27,17 +28,25 @@ class FakeServicioAudio extends ServicioAudio {
@override
Stream<EstadoReproduccion> get estadoStream => _estadoController.stream;
@override
bool get estaSonando => _estadoActual == EstadoReproduccion.reproduciendo;
@override
Future<void> reproducir(Emisora emisora) async {
_emisoraActual = emisora;
emisorasReproducidas.add(emisora);
_estadoController.add(EstadoReproduccion.reproduciendo);
emitirEstado(EstadoReproduccion.reproduciendo);
}
@override
Future<void> detener() async {
_emisoraActual = null;
_estadoController.add(EstadoReproduccion.detenido);
emitirEstado(EstadoReproduccion.detenido);
}
void emitirEstado(EstadoReproduccion estado) {
_estadoActual = estado;
_estadoController.add(estado);
}
@override