diff --git a/assets/generated/pluriwave-asset-sheet.png b/assets/generated/pluriwave-asset-sheet.png index fa8d6a9..a7e7e0a 100644 Binary files a/assets/generated/pluriwave-asset-sheet.png and b/assets/generated/pluriwave-asset-sheet.png differ diff --git a/assets/generated/pluriwave-night-ocean-asset-sheet.png b/assets/generated/pluriwave-night-ocean-asset-sheet.png new file mode 100644 index 0000000..a7e7e0a Binary files /dev/null and b/assets/generated/pluriwave-night-ocean-asset-sheet.png differ diff --git a/assets/generated/pluriwave-night-ocean-asset-sheet.prompt.md b/assets/generated/pluriwave-night-ocean-asset-sheet.prompt.md new file mode 100644 index 0000000..e9b940c --- /dev/null +++ b/assets/generated/pluriwave-night-ocean-asset-sheet.prompt.md @@ -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. diff --git a/assets/icons/pluri_favorites.png b/assets/icons/pluri_favorites.png index f4db54a..6cbf09f 100644 Binary files a/assets/icons/pluri_favorites.png and b/assets/icons/pluri_favorites.png differ diff --git a/assets/icons/pluri_home.png b/assets/icons/pluri_home.png index 7bcdd5c..1637bb4 100644 Binary files a/assets/icons/pluri_home.png and b/assets/icons/pluri_home.png differ diff --git a/assets/icons/pluri_player.png b/assets/icons/pluri_player.png index 6e53898..1b2966c 100644 Binary files a/assets/icons/pluri_player.png and b/assets/icons/pluri_player.png differ diff --git a/assets/icons/pluri_search.png b/assets/icons/pluri_search.png index a7c3495..9bc81c5 100644 Binary files a/assets/icons/pluri_search.png and b/assets/icons/pluri_search.png differ diff --git a/assets/icons/pluri_settings.png b/assets/icons/pluri_settings.png index f607d59..6364132 100644 Binary files a/assets/icons/pluri_settings.png and b/assets/icons/pluri_settings.png differ diff --git a/assets/icons/pluriwave_app_mark.png b/assets/icons/pluriwave_app_mark.png index a321006..744d69c 100644 Binary files a/assets/icons/pluriwave_app_mark.png and b/assets/icons/pluriwave_app_mark.png differ diff --git a/assets/images/aurora_wave_banner.png b/assets/images/aurora_wave_banner.png index b4dc033..cedb86e 100644 Binary files a/assets/images/aurora_wave_banner.png and b/assets/images/aurora_wave_banner.png differ diff --git a/assets/images/station_art_aurora.png b/assets/images/station_art_aurora.png index 3a34f4f..762ba43 100644 Binary files a/assets/images/station_art_aurora.png and b/assets/images/station_art_aurora.png differ diff --git a/assets/images/station_art_cosmic.png b/assets/images/station_art_cosmic.png index f206a45..a6aafd8 100644 Binary files a/assets/images/station_art_cosmic.png and b/assets/images/station_art_cosmic.png differ diff --git a/assets/images/station_art_nova.png b/assets/images/station_art_nova.png index a41ab10..d115e3c 100644 Binary files a/assets/images/station_art_nova.png and b/assets/images/station_art_nova.png differ diff --git a/assets/images/station_art_pulse.png b/assets/images/station_art_pulse.png index e8faca8..f7fd0d5 100644 Binary files a/assets/images/station_art_pulse.png and b/assets/images/station_art_pulse.png differ diff --git a/assets/mockups/pluriwave-night-ocean-mockup.png b/assets/mockups/pluriwave-night-ocean-mockup.png new file mode 100644 index 0000000..417fb58 Binary files /dev/null and b/assets/mockups/pluriwave-night-ocean-mockup.png differ diff --git a/assets/mockups/pluriwave-night-ocean-mockup.prompt.md b/assets/mockups/pluriwave-night-ocean-mockup.prompt.md new file mode 100644 index 0000000..8ef2155 --- /dev/null +++ b/assets/mockups/pluriwave-night-ocean-mockup.prompt.md @@ -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. diff --git a/lib/estado/estado_radio.dart b/lib/estado/estado_radio.dart index 993683e..2de230b 100644 --- a/lib/estado/estado_radio.dart +++ b/lib/estado/estado_radio.dart @@ -61,6 +61,7 @@ class EstadoRadio extends ChangeNotifier { bool _cargandoPopulares = false; bool _cargandoBusqueda = false; + EstadoReproduccion _estadoReproduccion = EstadoReproduccion.detenido; String? _errorCarga; List 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 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)); diff --git a/lib/pantallas/pantalla_buscar.dart b/lib/pantallas/pantalla_buscar.dart index 6f498d4..124b011 100644 --- a/lib/pantallas/pantalla_buscar.dart +++ b/lib/pantallas/pantalla_buscar.dart @@ -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 { itemBuilder: (context, i) => TarjetaEmisora( emisora: resultados[i], esCompacta: true, - onTap: () => context.read().reproducir(resultados[i]), + onTap: () => reproducirYAbrir(context, resultados[i]), ).animate().fadeIn(delay: (i * 20).ms).slideY(begin: 0.08), ); } diff --git a/lib/pantallas/pantalla_favoritos.dart b/lib/pantallas/pantalla_favoritos.dart index e7b1647..9dd52a3 100644 --- a/lib/pantallas/pantalla_favoritos.dart +++ b/lib/pantallas/pantalla_favoritos.dart @@ -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( diff --git a/lib/pantallas/pantalla_inicio.dart b/lib/pantallas/pantalla_inicio.dart index 53c79f5..ce6a1f7 100644 --- a/lib/pantallas/pantalla_inicio.dart +++ b/lib/pantallas/pantalla_inicio.dart @@ -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 { ), label: Text(e.nombre, maxLines: 1), onPressed: - () => context.read().reproducir(e), + () => reproducirYAbrir(context, e), ).animate().fadeIn(delay: (i * 50).ms); }, ), @@ -225,7 +227,7 @@ class _PantallaInicioState extends State { delegate: SliverChildBuilderDelegate( (context, i) => TarjetaEmisora( emisora: emisoras[i], - onTap: () => context.read().reproducir(emisoras[i]), + onTap: () => reproducirYAbrir(context, emisoras[i]), ).animate().fadeIn(delay: (i * 30).ms).slideY(begin: 0.1), childCount: emisoras.length, ), diff --git a/lib/pantallas/reproducir_y_abrir.dart b/lib/pantallas/reproducir_y_abrir.dart new file mode 100644 index 0000000..1935544 --- /dev/null +++ b/lib/pantallas/reproducir_y_abrir.dart @@ -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 reproducirYAbrir(BuildContext context, Emisora emisora) async { + final estado = context.read(); + unawaited(estado.reproducir(emisora)); + if (!context.mounted) return; + await PantallaReproductor.abrir(context, emisora); +} diff --git a/lib/servicios/servicio_audio.dart b/lib/servicios/servicio_audio.dart index 633801e..961a528 100644 --- a/lib/servicios/servicio_audio.dart +++ b/lib/servicios/servicio_audio.dart @@ -215,11 +215,15 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { @override Future 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); diff --git a/lib/tema/pluriwave_theme.dart b/lib/tema/pluriwave_theme.dart index fed63de..bee746f 100644 --- a/lib/tema/pluriwave_theme.dart +++ b/lib/tema/pluriwave_theme.dart @@ -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, ), diff --git a/lib/tema/pluriwave_tokens.dart b/lib/tema/pluriwave_tokens.dart index 01ffc7c..4fed783 100644 --- a/lib/tema/pluriwave_tokens.dart +++ b/lib/tema/pluriwave_tokens.dart @@ -37,12 +37,12 @@ class PluriWaveTokens extends ThemeExtension { 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, diff --git a/lib/widgets/pluri_wave_scaffold.dart b/lib/widgets/pluri_wave_scaffold.dart index 5b56d31..a6523be 100644 --- a/lib/widgets/pluri_wave_scaffold.dart +++ b/lib/widgets/pluri_wave_scaffold.dart @@ -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, ], diff --git a/test/estado/estado_radio_test.dart b/test/estado/estado_radio_test.dart index 0e5f2da..31ffd41 100644 --- a/test/estado/estado_radio_test.dart +++ b/test/estado/estado_radio_test.dart @@ -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.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')); diff --git a/test/helpers/fakes.dart b/test/helpers/fakes.dart index f79dbb0..bb3dd03 100644 --- a/test/helpers/fakes.dart +++ b/test/helpers/fakes.dart @@ -17,6 +17,7 @@ class FakeServicioAudio extends ServicioAudio { final List presetsAplicados = []; final List emisorasReproducidas = []; Emisora? _emisoraActual; + EstadoReproduccion _estadoActual = EstadoReproduccion.detenido; @override Emisora? get emisoraActual => _emisoraActual; @@ -27,17 +28,25 @@ class FakeServicioAudio extends ServicioAudio { @override Stream get estadoStream => _estadoController.stream; + @override + bool get estaSonando => _estadoActual == EstadoReproduccion.reproduciendo; + @override Future reproducir(Emisora emisora) async { _emisoraActual = emisora; emisorasReproducidas.add(emisora); - _estadoController.add(EstadoReproduccion.reproduciendo); + emitirEstado(EstadoReproduccion.reproduciendo); } @override Future detener() async { _emisoraActual = null; - _estadoController.add(EstadoReproduccion.detenido); + emitirEstado(EstadoReproduccion.detenido); + } + + void emitirEstado(EstadoReproduccion estado) { + _estadoActual = estado; + _estadoController.add(estado); } @override