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"; String instructions = ""; List contentWidgets = []; // Instrucciones específicas según navegador y dispositivo if (isIOS) { if (browser == 'safari') { instructions = "1. Toca el botón Compartir en la parte inferior de Safari (ícono de flecha hacia arriba)\n" "2. Desplázate hacia abajo y selecciona 'Añadir a pantalla de inicio'\n" "3. Confirma tocando 'Añadir'"; contentWidgets = [ Text("Para instalar esta aplicación en tu iPhone o iPad:"), SizedBox(height: 16), Text(instructions), SizedBox(height: 16), Text("Nota: La instalación de PWAs en iOS solo es compatible con Safari.", style: TextStyle(fontStyle: FontStyle.italic)), ]; } else { instructions = "Para instalar esta aplicación en iOS, debes abrir esta página en Safari."; contentWidgets = [ Text(instructions), SizedBox(height: 16), 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"; if (isAndroid) { instructions = "1. Toca el botón de menú (⋮) en la esquina superior derecha\n" "2. Selecciona 'Instalar aplicación' o 'Añadir a pantalla de inicio'\n" "3. Confirma la instalación"; } else { instructions = "1. Haz clic en el icono de instalación que aparece en la barra de direcciones (icono ▽)\n" "2. O bien, haz clic en el botón de menú (⋮) y selecciona 'Instalar CR-Civán...'\n" "3. Confirma la instalación en el diálogo que aparece"; } contentWidgets = [ Text("Para instalar esta aplicación en $browserName:"), SizedBox(height: 16), Text(instructions), ]; } else if (browser == 'firefox') { instructions = "1. Haz clic en el botón de menú (≡) en la esquina superior derecha\n" "2. Selecciona 'Instalar aplicación' (si está disponible)\n" "3. O añade manualmente esta página a tu pantalla de inicio"; contentWidgets = [ Text("Para instalar esta aplicación en Firefox:"), SizedBox(height: 16), Text(instructions), SizedBox(height: 16), Text("Nota: Firefox tiene soporte limitado para PWAs en algunos sistemas.", style: TextStyle(fontStyle: FontStyle.italic)), ]; } else { instructions = "1. Busca en el menú de tu navegador la opción 'Instalar aplicación' o 'Añadir a pantalla de inicio'\n" "2. También puedes usar las opciones de marcadores/favoritos para acceder rápidamente"; contentWidgets = [ Text("Para instalar esta aplicación:"), SizedBox(height: 16), Text(instructions), ]; } // Mostrar el diálogo con instrucciones showDialog( context: context, builder: (context) => AlertDialog( title: Text(title), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: contentWidgets, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text("ENTENDIDO"), ), ], ), ); } 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"), ), ], ); }, ); } double getResponsiveFontSize(BuildContext context, double baseFontSize) { double screenWidth = MediaQuery.of(context).size.width; if (screenWidth > 600) { return baseFontSize * 1.5; } else { return baseFontSize; } } @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, 12), ), ), ), SizedBox(height: isVerySmallScreen ? 5 : 8), ElevatedButton( onPressed: mostrarAvisoLegal, style: ElevatedButton.styleFrom( padding: EdgeInsets.symmetric( horizontal: 12, vertical: isVerySmallScreen ? 4 : 8 ), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( 'Aviso Legal', style: TextStyle( fontSize: getResponsiveFontSize(context, 12), ), ), ), 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 ? 14 : 16), label: Text( "Instalar aplicación", style: TextStyle( color: Colors.white, fontSize: isVerySmallScreen ? 12 : 14, fontWeight: FontWeight.bold, ), ), onPressed: _showInstallDialog, style: ElevatedButton.styleFrom( backgroundColor: Colors.green, padding: EdgeInsets.symmetric( horizontal: 12, vertical: isVerySmallScreen ? 4 : 8 ), 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, ), ), ), ), ), ); } }