import 'package:crcivan/BloC/contenedores_event.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:crcivan/bars/app_bar'; import 'package:crcivan/bars/bottom_bar'; import 'package:crcivan/pages/noticias'; import 'package:crcivan/pages/pregon'; import 'package:crcivan/pages/embalses'; import 'package:crcivan/pages/secciones'; import 'package:crcivan/widgets/background_widget.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:crcivan/pages/utils.dart'; import 'dart:html' as html; class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { html.BeforeInstallPromptEvent? deferredPrompt; bool isAppInstalled = false; @override void initState() { super.initState(); if (kIsWeb) { _detectInstallPrompt(); _checkIfAppInstalled(); } else { verificarPrimerInicio(); } } void _checkIfAppInstalled() { // Verificar si la app está ejecutándose en modo standalone (instalada) bool isStandalone = html.window.matchMedia('(display-mode: standalone)').matches; bool isIOSInstalled = false; try { // En iOS, navigator.standalone indica si se ejecuta como PWA isIOSInstalled = html.window.navigator.userAgent.contains('iPhone') && html.window.navigator.userAgent.contains('Safari') && !html.window.navigator.userAgent.contains('CriOS'); } catch (e) { // Ignorar errores si la propiedad no está disponible } setState(() { isAppInstalled = isStandalone || isIOSInstalled; }); // También escuchar cambios en el modo de visualización html.window.addEventListener('appinstalled', (event) { setState(() { isAppInstalled = true; deferredPrompt = null; }); }); } void _detectInstallPrompt() { html.window.addEventListener('beforeinstallprompt', (event) { // Prevenimos que el navegador muestre automáticamente el prompt event.preventDefault(); // Guardamos el evento para usarlo más tarde setState(() { deferredPrompt = event as html.BeforeInstallPromptEvent; }); print("Prompt de instalación capturado y listo para usar"); }); } void _showInstallDialog() { // Si el prompt está disponible, lo mostramos try { if (deferredPrompt != null) { // Muestra el diálogo de instalación deferredPrompt!.prompt(); // Esperamos la elección del usuario deferredPrompt!.userChoice.then((choiceResult) { // Limpiamos el prompt después de usarlo setState(() { deferredPrompt = null; }); if (kDebugMode) { if (choiceResult != null) { print("Usuario respondió al prompt: ${choiceResult['outcome']}"); } } // Si el usuario instaló la app, actualizamos el estado if (choiceResult != null && choiceResult['outcome'] == 'accepted') { setState(() { isAppInstalled = true; }); } // Configuramos para detectar un nuevo prompt _detectInstallPrompt(); }); } else { // Si no hay prompt disponible, mostramos instrucciones según el navegador _showBrowserSpecificInstructions(); } } catch (e) { print("Error al mostrar el diálogo de instalación: $e"); _showBrowserSpecificInstructions(); } } void _showBrowserSpecificInstructions() { final browserInfo = _detectBrowserAndDevice(); final String browser = browserInfo['browser']; final bool isIOS = browserInfo['isIOS']; final bool isAndroid = browserInfo['isAndroid']; String title = "Instala CR-Civán en tu dispositivo"; List contentWidgets = []; // Instrucciones específicas según navegador y dispositivo if (isIOS) { if (browser == 'safari') { contentWidgets = [ Text("Para instalar esta aplicación en tu iPhone o iPad:"), SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("1. "), Expanded( child: RichText( text: TextSpan( style: TextStyle(color: Colors.black), children: [ TextSpan(text: "Toca el botón Compartir "), WidgetSpan( child: Icon(Icons.ios_share, size: 18), alignment: PlaceholderAlignment.middle, ), TextSpan(text: " en la parte inferior de Safari"), ], ), ), ), ], ), SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("2. "), Expanded( child: Text("Desplázate hacia abajo y selecciona 'Añadir a pantalla de inicio'"), ), ], ), SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("3. "), Expanded(child: Text("Confirma tocando 'Añadir'")), ], ), SizedBox(height: 16), Text("Nota: La instalación de PWAs en iOS solo es compatible con Safari.", style: TextStyle(fontStyle: FontStyle.italic)), ]; } else { contentWidgets = [ Text("Para instalar esta aplicación en iOS, debes abrir esta página en Safari."), SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.info_outline, color: Colors.blue), SizedBox(width: 8), Flexible( child: Text("Las PWAs en iOS solo pueden instalarse desde Safari.", style: TextStyle(fontStyle: FontStyle.italic)), ), ], ), ]; } } else if (browser == 'chrome' || browser == 'edge') { String browserName = (browser == 'chrome') ? "Chrome" : "Edge"; IconData menuIcon = (browser == 'chrome') ? Icons.more_vert : Icons.more_horiz; if (isAndroid) { contentWidgets = [ Text("Para instalar esta aplicación en $browserName:"), SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("1. "), Expanded( child: RichText( text: TextSpan( style: TextStyle(color: Colors.black), children: [ TextSpan(text: "Toca el botón de menú "), WidgetSpan( child: Icon(menuIcon, size: 18), alignment: PlaceholderAlignment.middle, ), TextSpan(text: " en la esquina superior derecha"), ], ), ), ), ], ), SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("2. "), Expanded( child: RichText( text: TextSpan( style: TextStyle(color: Colors.black), children: [ TextSpan(text: "Selecciona 'Añadir a pantalla de inicio' "), WidgetSpan( child: Icon(Icons.add_to_home_screen, size: 18), alignment: PlaceholderAlignment.middle, ), ], ), ), ), ], ), SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("3. "), Expanded(child: Text("Confirma la instalación")), ], ), ]; } else { contentWidgets = [ Text("Para instalar esta aplicación en $browserName:"), SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("1. "), Expanded( child: RichText( text: TextSpan( style: TextStyle(color: Colors.black), children: [ TextSpan(text: "Haz clic en el icono de instalación "), WidgetSpan( child: Icon(Icons.download, size: 18), alignment: PlaceholderAlignment.middle, ), TextSpan(text: " que aparece en la barra de direcciones"), ], ), ), ), ], ), SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("2. "), Expanded( child: RichText( text: TextSpan( style: TextStyle(color: Colors.black), children: [ TextSpan(text: "O bien, haz clic en el botón de menú "), WidgetSpan( child: Icon(menuIcon, size: 18), alignment: PlaceholderAlignment.middle, ), TextSpan(text: " y selecciona 'Instalar CR-Civán...'"), ], ), ), ), ], ), SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("3. "), Expanded(child: Text("Confirma la instalación en el diálogo que aparece")), ], ), ]; } } else if (browser == 'firefox') { contentWidgets = [ Text("Para acceder rápidamente a esta aplicación en Firefox:"), SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("1. "), Expanded( child: RichText( text: TextSpan( style: TextStyle(color: Colors.black), children: [ TextSpan(text: "Toca el botón de menú "), WidgetSpan( child: Icon(Icons.menu, size: 18), alignment: PlaceholderAlignment.middle, ), TextSpan(text: " en la esquina superior derecha"), ], ), ), ), ], ), SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("2. "), Expanded( child: RichText( text: TextSpan( style: TextStyle(color: Colors.black), children: [ TextSpan(text: "Selecciona 'Añadir a marcadores' o 'Añadir a pantalla de inicio' "), WidgetSpan( child: Icon(Icons.bookmark_add, size: 18), alignment: PlaceholderAlignment.middle, ), ], ), ), ), ], ), SizedBox(height: 16), Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.orange.withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(Icons.info_outline, color: Colors.deepOrange, size: 20), SizedBox(width: 8), Expanded( child: Text( "Firefox tiene soporte limitado para instalar PWAs. Es posible que necesites usar Chrome para una instalación completa.", style: TextStyle(fontStyle: FontStyle.italic), ), ), ], ), ), ]; } else { contentWidgets = [ Text("Para instalar esta aplicación:"), SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("1. "), Expanded( child: RichText( text: TextSpan( style: TextStyle(color: Colors.black), children: [ TextSpan(text: "Busca en el menú de tu navegador "), WidgetSpan( child: Icon(Icons.menu, size: 18), alignment: PlaceholderAlignment.middle, ), TextSpan(text: " la opción 'Instalar aplicación' o 'Añadir a pantalla de inicio'"), ], ), ), ), ], ), SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("2. "), Expanded( child: RichText( text: TextSpan( style: TextStyle(color: Colors.black), children: [ TextSpan(text: "También puedes usar las opciones de marcadores "), WidgetSpan( child: Icon(Icons.bookmark, size: 18), alignment: PlaceholderAlignment.middle, ), TextSpan(text: " para acceder rápidamente"), ], ), ), ), ], ), ]; } // Mostrar el diálogo con instrucciones showDialog( context: context, builder: (context) { // Detectar si es pantalla pequeña final isSmallScreen = MediaQuery.of(context).size.width < 360; return AlertDialog( title: Row( children: [ Icon(Icons.install_mobile, color: Colors.green, size: isSmallScreen ? 18 : 24), SizedBox(width: 8), Flexible( child: Text( isSmallScreen ? "Instalar CR-Civán" : title, style: TextStyle( fontSize: isSmallScreen ? 16 : 18, fontWeight: FontWeight.bold, ), ), ), ], ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: contentWidgets, ), ), actions: [ TextButton.icon( icon: Icon(Icons.check, size: isSmallScreen ? 18 : 24), label: Text( "ENTENDIDO", style: TextStyle(fontSize: isSmallScreen ? 13 : 14), ), onPressed: () => Navigator.of(context).pop(), ), ], ); }, ); } Map _detectBrowserAndDevice() { String userAgent = html.window.navigator.userAgent.toLowerCase(); String browser = 'unknown'; bool isIOS = false; bool isAndroid = false; // Detectar sistema operativo if (userAgent.contains('iphone') || userAgent.contains('ipad') || userAgent.contains('ipod')) { isIOS = true; } else if (userAgent.contains('android')) { isAndroid = true; } // Detectar navegador if (userAgent.contains('chrome') && !userAgent.contains('edg')) { browser = 'chrome'; } else if (userAgent.contains('firefox')) { browser = 'firefox'; } else if (userAgent.contains('safari') && !userAgent.contains('chrome')) { browser = 'safari'; } else if (userAgent.contains('edg')) { browser = 'edge'; } else if (userAgent.contains('opr') || userAgent.contains('opera')) { browser = 'opera'; } return { 'browser': browser, 'isIOS': isIOS, 'isAndroid': isAndroid, }; } Future verificarPrimerInicio() async { SharedPreferences prefs = await SharedPreferences.getInstance(); bool yaMostrado = prefs.getBool('aviso_mostrado') ?? false; if (!yaMostrado) { Future.delayed(Duration.zero, () => mostrarAvisoLegal()); await prefs.setBool('aviso_mostrado', true); } } void mostrarAvisoLegal() { showDialog( context: context, barrierDismissible: false, builder: (context) { return AlertDialog( title: Text("Aviso Legal"), content: SingleChildScrollView( child: Text( "1. Relación con Entidades Oficiales:\n" "Esta aplicación no está afiliada, respaldada ni oficialmente conectada con ninguna entidad gubernamental, institución o autoridad pública. " "CR-Civán no representa a ninguna organización oficial ni gubernamental.\n\n" "2. Datos de los Embalses:\n" "Los datos sobre el estado de los embalses son proporcionados exclusivamente por la Confederación Hidrográfica del Ebro (CHE), " "los cuales están a disposición de todo el mundo en (https://www.saihebro.com/homepage/estado-cuenca-ebro). " "Los datos mostrados en esta aplicación no han sido alterados ni modificados; simplemente se presentan para mayor comodidad del usuario. " "CR-Civán no representa ni tiene relación directa con la Confederación Hidrográfica del Ebro y, por lo tanto, no se hace responsable de la exactitud o veracidad de los datos proporcionados por la CHE.\n\n" "3. Información Meteorológica:\n" "La información meteorológica se proporciona a través de un enlace a la página oficial de la Agencia Estatal de Meteorología (AEMET), " "disponible en (https://www.aemet.es). CR-Civán no se hace responsable de la exactitud de los datos proporcionados por AEMET, " "ya que la aplicación solo redirige al usuario a su página oficial sin modificar ni representar la información de manera directa. " "La información meteorológica se muestra en un navegador externo y no dentro de la propia aplicación, por lo que CR-Civán no se responsabiliza del contenido mostrado en dicho enlace.\n\n" "4. Responsabilidad sobre los Datos:\n" "Todos los datos presentados en esta aplicación provienen de fuentes externas y no han sido alterados ni modificados. " "CR-Civán no asume ninguna responsabilidad sobre la veracidad o exactitud de los mismos.", style: TextStyle(fontSize: getResponsiveFontSize(context, 16)), ), ), actions: [ ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Color.fromARGB(255, 255, 255, 255), ), onPressed: () { Navigator.of(context).pop(); }, child: Text("LO ENTIENDO"), ), ], ); }, ); } // Primero ajustamos la función de tamaño responsivo para que devuelva valores más grandes double getResponsiveFontSize(BuildContext context, double baseFontSize) { double screenWidth = MediaQuery.of(context).size.width; if (screenWidth > 600) { // Aumentamos el multiplicador para pantallas grandes return baseFontSize * 1.7; // Era 1.5 } else { // Para pantallas pequeñas usamos un valor ligeramente mayor al base return baseFontSize * 1.2; // Añadido multiplicador para pantallas pequeñas } } @override Widget build(BuildContext context) { // Obtener dimensiones de pantalla final screenSize = MediaQuery.of(context).size; final screenHeight = screenSize.height; // Hacer botones más pequeños con lógica adaptativa final isVerySmallScreen = screenHeight < 600; final isSmallScreen = screenHeight < 710; // Ajustar tamaños según la pantalla double buttonHeight = isVerySmallScreen ? 45 : (isSmallScreen ? 55 : 65); double buttonFontSize = isVerySmallScreen ? 12 : (isSmallScreen ? 14 : 18); double buttonBottomPadding = isVerySmallScreen ? 5.0 : (isSmallScreen ? 8.0 : 12.0); return Scaffold( appBar: const CustomAppBar(), body: SafeArea( child: LayoutBuilder( builder: (context, constraints) { // Ancho limitado para botones double buttonWidth = constraints.maxWidth * 0.8; if (buttonWidth > 300) buttonWidth = 300; return Stack( children: [ BackgroundWidget( child: Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), child: CustomScrollView( physics: ClampingScrollPhysics(), slivers: [ SliverFillRemaining( hasScrollBody: false, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Botones de navegación con altura reducida _buildNavButton( 'Avisos', Color.fromARGB(255, 240, 35, 35), () => Navigator.push(context, MaterialPageRoute( builder: (context) => const NoticiasPage())), buttonWidth, buttonHeight, buttonFontSize, buttonBottomPadding ), _buildNavButton( 'Pregón', Color.fromARGB(255, 230, 226, 0), () => Navigator.push(context, MaterialPageRoute( builder: (context) => const Pregon())), buttonWidth, buttonHeight, buttonFontSize, buttonBottomPadding ), _buildNavButton( 'Embalses', Color.fromARGB(255, 6, 71, 169), () => Navigator.push(context, MaterialPageRoute( builder: (context) => const EmbalsesPage())), buttonWidth, buttonHeight, buttonFontSize, buttonBottomPadding ), _buildNavButton( 'Secciones', Color.fromARGB(255, 97, 97, 97), () => Navigator.push(context, MaterialPageRoute( builder: (context) => const SeccionesPage())), buttonWidth, buttonHeight, buttonFontSize, buttonBottomPadding ), _buildNavButton( 'Tiempo', Color.fromARGB(255, 255, 255, 255), launchAemetURL, buttonWidth, buttonHeight, buttonFontSize, buttonBottomPadding, textColor: Color.fromARGB(255, 78, 169, 6) ), SizedBox(height: isVerySmallScreen ? 5 : 10), // Links y botones inferiores con tamaños reducidos InkWell( onTap: launchPrivacyPolicyURL, child: Text( 'Lee nuestras políticas de privacidad', style: TextStyle( color: Colors.blue, decoration: TextDecoration.underline, fontSize: getResponsiveFontSize(context, 14), // Aumentado de 12 a 14 fontWeight: FontWeight.w500, // Añadido para mejorar legibilidad ), ), ), SizedBox(height: isVerySmallScreen ? 5 : 8), ElevatedButton( onPressed: mostrarAvisoLegal, style: ElevatedButton.styleFrom( padding: EdgeInsets.symmetric( horizontal: 16, // Aumentado de 12 a 16 vertical: isVerySmallScreen ? 6 : 10 // Aumentado de 4:8 a 6:10 ), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( 'Aviso Legal', style: TextStyle( fontSize: getResponsiveFontSize(context, 14), // Aumentado de 12 a 14 fontWeight: FontWeight.w500, // Añadido para mejor legibilidad ), ), ), SizedBox(height: isVerySmallScreen ? 5 : 8), // Botón de instalación con tamaño reducido - solo se muestra si es web y no está instalada if (kIsWeb && !isAppInstalled) ElevatedButton.icon( icon: Icon(Icons.download_rounded, color: Colors.white, size: isVerySmallScreen ? 16 : 18), // Aumentado de 14:16 a 16:18 label: Text( "Instalar aplicación", style: TextStyle( color: Colors.white, fontSize: isVerySmallScreen ? 14 : 16, // Aumentado de 12:14 a 14:16 fontWeight: FontWeight.bold, ), ), onPressed: _showInstallDialog, style: ElevatedButton.styleFrom( backgroundColor: Colors.green, padding: EdgeInsets.symmetric( horizontal: 16, // Aumentado de 12 a 16 vertical: isVerySmallScreen ? 6 : 10 // Aumentado de 4:8 a 6:10 ), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), SizedBox(height: 8), ], ), ), ), ], ), ), ), ], ); }, ), ), bottomNavigationBar: const CustomBottomBar(), ); } // Widget para botones de navegación Widget _buildNavButton( String title, Color color, VoidCallback onTap, double width, double height, double fontSize, double bottomPadding, {Color textColor = Colors.white}) { return Padding( padding: EdgeInsets.only(bottom: bottomPadding), child: InkWell( onTap: onTap, child: Container( width: width, height: height, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(7.0), boxShadow: [ BoxShadow( color: Colors.black26, blurRadius: 2, offset: Offset(0, 1), ), ], ), child: Center( child: Text( title, textAlign: TextAlign.center, style: TextStyle( color: textColor, fontSize: fontSize, fontWeight: FontWeight.bold, ), ), ), ), ), ); } }