import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'estado/estado_radio.dart'; import 'estado/estado_alarmas.dart'; import 'modelos/alarma_musical.dart'; import 'pantallas/pantalla_alarmas.dart'; import 'pantallas/pantalla_alarma_sonando.dart'; import 'pantallas/pantalla_inicio.dart'; import 'pantallas/pantalla_buscar.dart'; import 'pantallas/pantalla_favoritos.dart'; import 'pantallas/pantalla_ajustes.dart'; import 'tema/pluriwave_theme.dart'; import 'widgets/pluri_glass_surface.dart'; import 'widgets/pluri_icon.dart'; import 'widgets/pluri_wave_scaffold.dart'; import 'package:pluriwave/widgets/mini_reproductor.dart'; import 'servicios/servicio_alarmas_android.dart'; class PluriWaveApp extends StatelessWidget { const PluriWaveApp({super.key}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => EstadoRadio()), ChangeNotifierProvider(create: (_) => EstadoAlarmas()), ], child: MaterialApp( title: 'PluriWave', debugShowCheckedModeBanner: false, theme: PluriWaveTheme.dark(), darkTheme: PluriWaveTheme.dark(), themeMode: ThemeMode.dark, home: const _PaginaPrincipal(), ), ); } } class _PaginaPrincipal extends StatefulWidget { const _PaginaPrincipal(); @override State<_PaginaPrincipal> createState() => _PaginaPrincipalState(); } class _PaginaPrincipalState extends State<_PaginaPrincipal> { int _indice = 0; StreamSubscription? _errorSubscription; StreamSubscription? _alarmaSubscription; StreamSubscription? _alarmaVencidaSubscription; EstadoRadio? _estadoSuscrito; bool _alarmaInicialProcesada = false; static const _paginas = [ PantallaInicio(), PantallaBuscar(), PantallaFavoritos(), PantallaAlarmas(), PantallaAjustes(), ]; static const _destinos = [ NavigationDestination( icon: PluriIcon(glyph: PluriIconGlyph.home), selectedIcon: PluriIcon( glyph: PluriIconGlyph.home, variant: PluriIconVariant.activeGlow, ), label: 'Inicio', ), NavigationDestination( icon: PluriIcon(glyph: PluriIconGlyph.search), selectedIcon: PluriIcon( glyph: PluriIconGlyph.search, variant: PluriIconVariant.activeGlow, ), label: 'Buscar', ), NavigationDestination( icon: PluriIcon(glyph: PluriIconGlyph.favorites), selectedIcon: PluriIcon( glyph: PluriIconGlyph.favorites, variant: PluriIconVariant.activeGlow, ), label: 'Favoritos', ), NavigationDestination( icon: PluriIcon(glyph: PluriIconGlyph.alarm), selectedIcon: PluriIcon( glyph: PluriIconGlyph.alarm, variant: PluriIconVariant.activeGlow, ), label: 'Alarmas', ), NavigationDestination( icon: PluriIcon(glyph: PluriIconGlyph.settings), selectedIcon: PluriIcon( glyph: PluriIconGlyph.settings, variant: PluriIconVariant.activeGlow, ), label: 'Ajustes', ), ]; @override void didChangeDependencies() { super.didChangeDependencies(); final estado = context.read(); if (identical(_estadoSuscrito, estado) && _errorSubscription != null) { return; } _errorSubscription?.cancel(); _estadoSuscrito = estado; _errorSubscription = estado.errorStream.listen((msg) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(msg), duration: const Duration(seconds: 3), action: SnackBarAction(label: 'OK', onPressed: () {}), ), ); }); final alarmas = context.read(); _alarmaSubscription ??= alarmas.android.eventosAlarma.listen((evento) { if (!mounted) return; _abrirAlarmaSonando(evento); }); _alarmaVencidaSubscription ??= alarmas.alarmasVencidasStream.listen(( alarma, ) { if (!mounted) return; _abrirAlarmaDirecta(alarma); }); if (!_alarmaInicialProcesada) { _alarmaInicialProcesada = true; unawaited(_procesarAlarmaInicial(alarmas)); } } @override void dispose() { _errorSubscription?.cancel(); _alarmaSubscription?.cancel(); _alarmaVencidaSubscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return PluriWaveScaffold( appBar: AppBar( title: const Text('PluriWave'), actions: [ IconButton( icon: const Icon(Icons.bedtime_outlined), tooltip: 'Timer de sueno', onPressed: () => _mostrarTimerDialog(context), ), ], ), body: SafeArea(top: false, child: _paginas[_indice]), bottomNavigationBar: SafeArea( top: false, minimum: const EdgeInsets.only(bottom: 8), child: Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), child: Column( mainAxisSize: MainAxisSize.min, children: [ const MiniReproductor(), PluriGlassSurface( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), borderRadius: BorderRadius.circular(999), child: NavigationBar( selectedIndex: _indice, height: 66, onDestinationSelected: (i) => setState(() => _indice = i), destinations: _destinos, ), ), ], ), ), ), ); } Future _procesarAlarmaInicial(EstadoAlarmas alarmas) async { final evento = await alarmas.android.obtenerEventoInicial(); if (evento != null && mounted) { await _abrirAlarmaSonando(evento); } } Future _abrirAlarmaSonando(EventoAlarmaAndroid evento) async { final estado = context.read(); await estado.refrescarProgramacion(); AlarmaMusical? alarma; for (final item in estado.alarmas) { if (item.id == evento.alarmaId) { alarma = item; break; } } if (alarma == null || !mounted) return; if (evento.accion.endsWith('.SKIP_NEXT')) { await estado.saltarProxima(alarma.id); if (!mounted) return; setState(() => _indice = 3); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Omitida esta ejecución de ${alarma.nombre}.'), ), ); return; } if (evento.accion.endsWith('.PRE_NOTICE')) { setState(() => _indice = 3); return; } await Navigator.of(context).push( MaterialPageRoute( builder: (_) => PantallaAlarmaSonando(alarma: alarma!), fullscreenDialog: true, ), ); } Future _abrirAlarmaDirecta(AlarmaMusical alarma) async { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => PantallaAlarmaSonando(alarma: alarma), fullscreenDialog: true, ), ); } void _mostrarTimerDialog(BuildContext context) { final estado = context.read(); showModalBottomSheet( context: context, builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Timer de sueño', style: Theme.of(ctx).textTheme.titleLarge, ), const SizedBox(height: 16), if (estado.timer.activo) StreamBuilder( stream: estado.timer.tiempoRestanteStream, builder: (ctx, snap) { final t = snap.data ?? Duration.zero; final h = t.inHours; final m = t.inMinutes .remainder(60) .toString() .padLeft(2, '0'); final s = t.inSeconds .remainder(60) .toString() .padLeft(2, '0'); return Column( children: [ Text( '${h > 0 ? "${h}h " : ""}${m}m ${s}s', style: Theme.of(ctx).textTheme.headlineMedium, ), const SizedBox(height: 8), FilledButton.tonal( onPressed: () { estado.cancelarTimer(); Navigator.pop(ctx); }, child: const Text('Cancelar timer'), ), ], ); }, ) else Wrap( spacing: 8, children: [3, 5, 10, 15, 30, 60, 90, 120, 180] .map( (min) => ActionChip( label: Text('$min min'), onPressed: () { estado.iniciarTimer(min); Navigator.pop(ctx); }, ), ) .toList(), ), ], ), ), ), ); } }