fix(player): stabilize first playback and refresh design
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.5 MiB |
|
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.
|
||||||
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 565 KiB After Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 436 KiB After Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 487 KiB After Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 484 KiB After Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 351 KiB |
|
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.
|
||||||
@@ -61,6 +61,7 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
|
|
||||||
bool _cargandoPopulares = false;
|
bool _cargandoPopulares = false;
|
||||||
bool _cargandoBusqueda = false;
|
bool _cargandoBusqueda = false;
|
||||||
|
EstadoReproduccion _estadoReproduccion = EstadoReproduccion.detenido;
|
||||||
String? _errorCarga;
|
String? _errorCarga;
|
||||||
|
|
||||||
List<Emisora> get populares => _populares;
|
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.
|
/// Escucha el stream de estado del audio y gestiona errores de reproducción.
|
||||||
void _escucharErroresReproduccion() {
|
void _escucharErroresReproduccion() {
|
||||||
_suscripcionEstadoAudio = audio.estadoStream.listen((estado) {
|
_suscripcionEstadoAudio = audio.estadoStream.listen((estado) {
|
||||||
if (estado == EstadoReproduccion.error) {
|
_estadoReproduccion = estado;
|
||||||
if (timer.activo) {
|
if (estado == EstadoReproduccion.error && timer.activo) {
|
||||||
unawaited(timer.cancelar());
|
unawaited(timer.cancelar());
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
notifyListeners();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +192,19 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reproducir(Emisora emisora) async {
|
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 {
|
try {
|
||||||
|
notifyListeners();
|
||||||
await audio.reproducir(emisora);
|
await audio.reproducir(emisora);
|
||||||
unawaited(radio.registrarClick(emisora.uuid));
|
unawaited(radio.registrarClick(emisora.uuid));
|
||||||
await _aplicarPresetActivo(_presetParaEmisora(emisora.uuid));
|
await _aplicarPresetActivo(_presetParaEmisora(emisora.uuid));
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import '../widgets/pluri_icon.dart';
|
|||||||
import '../widgets/pluri_premium_widgets.dart';
|
import '../widgets/pluri_premium_widgets.dart';
|
||||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||||
|
|
||||||
|
import 'reproducir_y_abrir.dart';
|
||||||
|
|
||||||
const _paises = [
|
const _paises = [
|
||||||
('Espana', 'ES'),
|
('Espana', 'ES'),
|
||||||
('USA', 'US'),
|
('USA', 'US'),
|
||||||
@@ -200,7 +202,7 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
|||||||
itemBuilder: (context, i) => TarjetaEmisora(
|
itemBuilder: (context, i) => TarjetaEmisora(
|
||||||
emisora: resultados[i],
|
emisora: resultados[i],
|
||||||
esCompacta: true,
|
esCompacta: true,
|
||||||
onTap: () => context.read<EstadoRadio>().reproducir(resultados[i]),
|
onTap: () => reproducirYAbrir(context, resultados[i]),
|
||||||
).animate().fadeIn(delay: (i * 20).ms).slideY(begin: 0.08),
|
).animate().fadeIn(delay: (i * 20).ms).slideY(begin: 0.08),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import '../widgets/pluri_icon.dart';
|
|||||||
import '../widgets/pluri_premium_widgets.dart';
|
import '../widgets/pluri_premium_widgets.dart';
|
||||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||||
|
|
||||||
|
import 'reproducir_y_abrir.dart';
|
||||||
|
|
||||||
class PantallaFavoritos extends StatelessWidget {
|
class PantallaFavoritos extends StatelessWidget {
|
||||||
const PantallaFavoritos({super.key});
|
const PantallaFavoritos({super.key});
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@ class PantallaFavoritos extends StatelessWidget {
|
|||||||
key: Key(emisora.uuid),
|
key: Key(emisora.uuid),
|
||||||
emisora: emisora,
|
emisora: emisora,
|
||||||
esCompacta: true,
|
esCompacta: true,
|
||||||
onTap: () => estado.reproducir(emisora),
|
onTap: () => reproducirYAbrir(context, emisora),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton.filledTonal(
|
IconButton.filledTonal(
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import '../widgets/pluri_icon.dart';
|
|||||||
import '../widgets/pluri_premium_widgets.dart';
|
import '../widgets/pluri_premium_widgets.dart';
|
||||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||||
|
|
||||||
|
import 'reproducir_y_abrir.dart';
|
||||||
|
|
||||||
/// Pantalla principal: emisoras populares y por género.
|
/// Pantalla principal: emisoras populares y por género.
|
||||||
class PantallaInicio extends StatefulWidget {
|
class PantallaInicio extends StatefulWidget {
|
||||||
const PantallaInicio({super.key});
|
const PantallaInicio({super.key});
|
||||||
@@ -118,7 +120,7 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
|||||||
),
|
),
|
||||||
label: Text(e.nombre, maxLines: 1),
|
label: Text(e.nombre, maxLines: 1),
|
||||||
onPressed:
|
onPressed:
|
||||||
() => context.read<EstadoRadio>().reproducir(e),
|
() => reproducirYAbrir(context, e),
|
||||||
).animate().fadeIn(delay: (i * 50).ms);
|
).animate().fadeIn(delay: (i * 50).ms);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -225,7 +227,7 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
|||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(context, i) => TarjetaEmisora(
|
(context, i) => TarjetaEmisora(
|
||||||
emisora: emisoras[i],
|
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),
|
).animate().fadeIn(delay: (i * 30).ms).slideY(begin: 0.1),
|
||||||
childCount: emisoras.length,
|
childCount: emisoras.length,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -215,11 +215,15 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
|||||||
@override
|
@override
|
||||||
Future<void> playMediaItem(MediaItem mediaItem) async {
|
Future<void> playMediaItem(MediaItem mediaItem) async {
|
||||||
this.mediaItem.add(mediaItem);
|
this.mediaItem.add(mediaItem);
|
||||||
|
emisoraActual = _emisoraDesdeMediaItem(mediaItem);
|
||||||
|
playbackState.add(playbackState.value.copyWith(
|
||||||
|
processingState: AudioProcessingState.loading,
|
||||||
|
playing: false,
|
||||||
|
));
|
||||||
try {
|
try {
|
||||||
await _player.stop();
|
await _player.stop();
|
||||||
await _player.setUrl(mediaItem.id);
|
await _player.setUrl(mediaItem.id);
|
||||||
await _player.play();
|
await _player.play();
|
||||||
emisoraActual = _emisoraDesdeMediaItem(mediaItem);
|
|
||||||
await _activarEcualizador();
|
await _activarEcualizador();
|
||||||
} on PlayerException catch (e) {
|
} on PlayerException catch (e) {
|
||||||
_gestionarErrorReproduccion(e);
|
_gestionarErrorReproduccion(e);
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ abstract final class PluriWaveTheme {
|
|||||||
const tokens = PluriWaveTokens.dark;
|
const tokens = PluriWaveTokens.dark;
|
||||||
final colorScheme = const ColorScheme.dark().copyWith(
|
final colorScheme = const ColorScheme.dark().copyWith(
|
||||||
primary: tokens.electricMagenta,
|
primary: tokens.electricMagenta,
|
||||||
secondary: const Color(0xFF20E6FF),
|
secondary: const Color(0xFF7EE4C2),
|
||||||
tertiary: tokens.warmCoral,
|
tertiary: tokens.warmCoral,
|
||||||
surface: const Color(0xFF0B1024),
|
surface: const Color(0xFF0D1B24),
|
||||||
surfaceContainerLow: const Color(0xFF111831),
|
surfaceContainerLow: const Color(0xFF102532),
|
||||||
surfaceContainerHighest: const Color(0xFF202946),
|
surfaceContainerHighest: const Color(0xFF1B3942),
|
||||||
onSurface: const Color(0xFFF7F2FF),
|
onSurface: const Color(0xFFF2F7FA),
|
||||||
onPrimary: Colors.white,
|
onPrimary: Colors.white,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ abstract final class PluriWaveTheme {
|
|||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
foregroundColor: Color(0xFFF7F2FF),
|
foregroundColor: Color(0xFFF2F7FA),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
scrolledUnderElevation: 0,
|
scrolledUnderElevation: 0,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ class PluriWaveTokens extends ThemeExtension<PluriWaveTokens> {
|
|||||||
final double spacingLg;
|
final double spacingLg;
|
||||||
|
|
||||||
static const dark = PluriWaveTokens(
|
static const dark = PluriWaveTokens(
|
||||||
deepViolet: Color(0xFF070A18),
|
deepViolet: Color(0xFF07121A),
|
||||||
electricMagenta: Color(0xFFFF3DF2),
|
electricMagenta: Color(0xFF21D4D9),
|
||||||
warmCoral: Color(0xFFFFB86B),
|
warmCoral: Color(0xFFF4B860),
|
||||||
glassSurface: Color(0x24FFFFFF),
|
glassSurface: Color(0x1FFFFFFF),
|
||||||
glassBorder: Color(0x52FFFFFF),
|
glassBorder: Color(0x33FFFFFF),
|
||||||
glowColor: Color(0x88FF3DF2),
|
glowColor: Color(0x6621D4D9),
|
||||||
radiusSm: 14,
|
radiusSm: 14,
|
||||||
radiusMd: 22,
|
radiusMd: 22,
|
||||||
radiusLg: 30,
|
radiusLg: 30,
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ class PluriWaveScaffold extends StatelessWidget {
|
|||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [
|
||||||
const Color(0xFF070A18),
|
const Color(0xFF07121A),
|
||||||
t.deepViolet,
|
const Color(0xFF0D1B24),
|
||||||
const Color(0xFF151033),
|
const Color(0xFF0E4A4F),
|
||||||
const Color(0xFF070A18),
|
const Color(0xFF07121A),
|
||||||
],
|
],
|
||||||
stops: const [0, 0.34, 0.68, 1],
|
stops: const [0, 0.34, 0.68, 1],
|
||||||
),
|
),
|
||||||
@@ -45,17 +45,17 @@ class PluriWaveScaffold extends StatelessWidget {
|
|||||||
Positioned(
|
Positioned(
|
||||||
left: -120,
|
left: -120,
|
||||||
top: -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(
|
Positioned(
|
||||||
right: -150,
|
right: -150,
|
||||||
top: 160,
|
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(
|
Positioned(
|
||||||
left: -90,
|
left: -90,
|
||||||
bottom: 80,
|
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,
|
body,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:pluriwave/estado/estado_radio.dart';
|
import 'package:pluriwave/estado/estado_radio.dart';
|
||||||
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||||
|
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||||
|
|
||||||
import '../helpers/fakes.dart';
|
import '../helpers/fakes.dart';
|
||||||
|
|
||||||
@@ -172,6 +173,49 @@ void main() {
|
|||||||
expect(audio.presetsAplicados.last, principal);
|
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 {
|
test('reordenar favoritos reindexa de forma determinística', () async {
|
||||||
final favoritos = FakeServicioFavoritos();
|
final favoritos = FakeServicioFavoritos();
|
||||||
await favoritos.agregar(emisoraDemo(uuid: 'a', nombre: 'A'));
|
await favoritos.agregar(emisoraDemo(uuid: 'a', nombre: 'A'));
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class FakeServicioAudio extends ServicioAudio {
|
|||||||
final List<PresetEcualizador> presetsAplicados = [];
|
final List<PresetEcualizador> presetsAplicados = [];
|
||||||
final List<Emisora> emisorasReproducidas = [];
|
final List<Emisora> emisorasReproducidas = [];
|
||||||
Emisora? _emisoraActual;
|
Emisora? _emisoraActual;
|
||||||
|
EstadoReproduccion _estadoActual = EstadoReproduccion.detenido;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Emisora? get emisoraActual => _emisoraActual;
|
Emisora? get emisoraActual => _emisoraActual;
|
||||||
@@ -27,17 +28,25 @@ class FakeServicioAudio extends ServicioAudio {
|
|||||||
@override
|
@override
|
||||||
Stream<EstadoReproduccion> get estadoStream => _estadoController.stream;
|
Stream<EstadoReproduccion> get estadoStream => _estadoController.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get estaSonando => _estadoActual == EstadoReproduccion.reproduciendo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> reproducir(Emisora emisora) async {
|
Future<void> reproducir(Emisora emisora) async {
|
||||||
_emisoraActual = emisora;
|
_emisoraActual = emisora;
|
||||||
emisorasReproducidas.add(emisora);
|
emisorasReproducidas.add(emisora);
|
||||||
_estadoController.add(EstadoReproduccion.reproduciendo);
|
emitirEstado(EstadoReproduccion.reproduciendo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> detener() async {
|
Future<void> detener() async {
|
||||||
_emisoraActual = null;
|
_emisoraActual = null;
|
||||||
_estadoController.add(EstadoReproduccion.detenido);
|
emitirEstado(EstadoReproduccion.detenido);
|
||||||
|
}
|
||||||
|
|
||||||
|
void emitirEstado(EstadoReproduccion estado) {
|
||||||
|
_estadoActual = estado;
|
||||||
|
_estadoController.add(estado);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||