import 'dart:async'; import 'package:flutter/material.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/services.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:manager_api_new/api.dart'; import 'package:mymuseum_visitapp/Components/AssistantChatSheet.dart'; import 'package:mymuseum_visitapp/Components/CustomAppBar.dart'; import 'package:mymuseum_visitapp/Components/LanguageSelection.dart'; import 'package:mymuseum_visitapp/Components/ScannerBouton.dart'; import 'package:mymuseum_visitapp/Components/loading_common.dart'; import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart'; import 'package:mymuseum_visitapp/Helpers/modelsHelper.dart'; import 'package:mymuseum_visitapp/Helpers/networkCheck.dart'; import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart'; import 'package:mymuseum_visitapp/Helpers/translationHelper.dart'; import 'package:mymuseum_visitapp/Models/beaconSection.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart'; import 'package:mymuseum_visitapp/Screens/ConfigurationPage/configuration_page.dart'; import 'package:mymuseum_visitapp/Services/apiService.dart'; import 'package:mymuseum_visitapp/Services/downloadConfiguration.dart'; import 'package:mymuseum_visitapp/Services/statisticsService.dart'; import 'package:mymuseum_visitapp/app_context.dart'; import 'package:mymuseum_visitapp/client.dart'; import 'package:mymuseum_visitapp/constants.dart'; import 'package:provider/provider.dart'; class HomePage3 extends StatefulWidget { const HomePage3({Key? key}) : super(key: key); @override State createState() => _HomePage3State(); } class _HomePage3State extends State with WidgetsBindingObserver { int currentIndex = 0; late List configurations = []; List alreadyDownloaded = []; late VisitAppContext visitAppContext; late Future?> _futureConfigurations; @override void initState() { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); super.initState(); final appContext = Provider.of(context, listen: false); _futureConfigurations = getConfigurationsCall(appContext); } Widget _buildCard(BuildContext context, int index) { final lang = visitAppContext.language ?? "FR"; final config = configurations[index]; final titleEntry = config.title?.firstWhere( (t) => t.language == lang, orElse: () => config.title!.first, ); final cleanedTitle = (titleEntry?.value ?? '').replaceAll('\n', ' ').replaceAll('
', ' '); return InkWell( borderRadius: BorderRadius.circular(16), onTap: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) => ConfigurationPage( configuration: config, isAlreadyAllowed: visitAppContext.isScanBeaconAlreadyAllowed, ), )); }, child: Hero( tag: config.id!, child: Material( type: MaterialType.transparency, child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Container( decoration: BoxDecoration( color: kSecondGrey, borderRadius: BorderRadius.circular(16), boxShadow: const [ BoxShadow( color: Colors.black38, blurRadius: 6, offset: Offset(0, 2), ), ], image: config.imageSource != null ? DecorationImage( fit: BoxFit.cover, image: NetworkImage(config.imageSource!), ) : null, ), child: Stack( children: [ // Gradient overlay for text readability Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black.withValues(alpha: 0.72), ], stops: const [0.4, 1.0], ), ), ), ), // Title at bottom-left Positioned( bottom: 10, left: 10, right: 28, child: HtmlWidget( cleanedTitle, textStyle: const TextStyle( color: Colors.white, fontFamily: 'Roboto', fontSize: 14, fontWeight: FontWeight.w600, ), customStylesBuilder: (_) => { 'font-family': 'Roboto', '-webkit-line-clamp': '2', }, ), ), // Chevron const Positioned( bottom: 10, right: 8, child: Icon(Icons.chevron_right, size: 18, color: Colors.white), ), ], ), ), ), ), ), ); } @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; final appContext = Provider.of(context); visitAppContext = appContext.getContext(); return Scaffold( extendBody: true, body: FutureBuilder( future: _futureConfigurations, builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { configurations = List.from(snapshot.data) .where((c) => c.isMobile!) .toList(); final layoutType = visitAppContext.applicationInstanceDTO?.layoutMainPage; final isMasonry = layoutType == null || layoutType.value == LayoutMainPageType.MasonryGrid.value; final lang = visitAppContext.language ?? "FR"; final headerTitleEntry = configurations.isNotEmpty ? configurations[0].title?.firstWhere( (t) => t.language == lang, orElse: () => configurations[0].title!.first, ) : null; return Stack( children: [ // Dark background const ColoredBox(color: Color(0xFF111111), child: SizedBox.expand()), SafeArea( top: false, bottom: false, child: CustomScrollView( slivers: [ SliverAppBar( backgroundColor: Colors.transparent, pinned: false, expandedHeight: 235.0, flexibleSpace: FlexibleSpaceBar( collapseMode: CollapseMode.pin, centerTitle: true, background: Container( padding: const EdgeInsets.only(bottom: 25.0), decoration: const BoxDecoration( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(25), bottomRight: Radius.circular(25), ), boxShadow: [ BoxShadow( color: Colors.black38, spreadRadius: 0.5, blurRadius: 8, offset: Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(25), bottomRight: Radius.circular(25), ), child: Stack( fit: StackFit.expand, children: [ if (configurations.isNotEmpty && configurations[0].imageSource != null) Image.network( configurations[0].imageSource!, fit: BoxFit.cover, ), // Bottom gradient for title readability const DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.transparent, Colors.black54], stops: [0.5, 1.0], ), ), ), Positioned( top: 35, right: 10, child: SizedBox( width: 75, height: 75, child: LanguageSelection(), ), ), ], ), ), ), title: SizedBox( width: size.width * 1.0, height: 120, child: Center( child: headerTitleEntry != null ? HtmlWidget( headerTitleEntry.value!, textStyle: const TextStyle( color: Colors.white, fontFamily: 'Roboto', fontSize: 20, fontWeight: FontWeight.bold, ), customStylesBuilder: (_) => { 'text-align': 'center', 'font-family': 'Roboto', '-webkit-line-clamp': '2', }, ) : const SizedBox(), ), ), ), ), SliverPadding( padding: const EdgeInsets.only( left: 8.0, right: 8.0, top: 8.0, bottom: 20.0), sliver: isMasonry ? SliverMasonryGrid.count( crossAxisCount: 2, mainAxisSpacing: 12, crossAxisSpacing: 12, childCount: configurations.length, itemBuilder: (context, index) => SizedBox( height: 160 + (index % 3) * 50, child: _buildCard(context, index), ), ) : SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 0.82, ), delegate: SliverChildBuilderDelegate( _buildCard, childCount: configurations.length, ), ), ), ], ), ), if (visitAppContext.applicationInstanceDTO?.isAssistant == true) Positioned( bottom: 24, right: 16, child: FloatingActionButton( heroTag: 'assistant_home', backgroundColor: kMainColor1, onPressed: () { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => AssistantChatSheet( visitAppContext: visitAppContext, onNavigateToSection: (configurationId, _) { final config = configurations .where((c) => c.id == configurationId) .firstOrNull; if (config != null) { Navigator.push( context, MaterialPageRoute( builder: (_) => ConfigurationPage( configuration: config, isAlreadyAllowed: visitAppContext.isScanBeaconAlreadyAllowed, ), ), ); } }, ), ); }, child: const Icon(Icons.chat_bubble_outline, color: Colors.white), ), ), ], ); } else if (snapshot.connectionState == ConnectionState.none) { return Text(TranslationHelper.getFromLocale("noData", appContext.getContext())); } else { return Center( child: SizedBox( height: size.height * 0.15, child: const LoadingCommon(), ), ); } }, ), ); } Future?> getConfigurationsCall(AppContext appContext) async { bool isOnline = await hasNetwork(); VisitAppContext visitAppContext = appContext.getContext(); isOnline = true; // Todo remove if not local test List? configurations; configurations = List.from(await DatabaseHelper.instance.getData(DatabaseTableType.configurations)); alreadyDownloaded = configurations.map((c) => c.id).toList(); print("GOT configurations from LOCAL"); print(configurations.length); print(configurations); if(!isOnline) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(TranslationHelper.getFromLocale("noInternet", appContext.getContext())), backgroundColor: kMainColor2), ); // GET ALL SECTIONIDS FOR ALL CONFIGURATION (OFFLINE) for(var configuration in configurations) { var sections = List.from(await DatabaseHelper.instance.queryWithConfigurationId(DatabaseTableType.sections, configuration.id!)); configuration.sectionIds = sections.map((e) => e.id!).toList(); } // GET BEACONS FROM LOCAL List beaconSections = List.from(await DatabaseHelper.instance.getData(DatabaseTableType.beaconSection)); print("GOT beaconSection from LOCAL"); print(beaconSections); visitAppContext.beaconSections = beaconSections; //appContext.setContext(visitAppContext); return configurations; } if(visitAppContext.beaconSections == null) { List? sections = await ApiService.getAllBeacons(visitAppContext.clientAPI, visitAppContext.instanceId!); if(sections != null && sections.isNotEmpty) { List beaconSections = sections.map((e) => BeaconSection(minorBeaconId: e.beaconId, orderInConfig: e.order, configurationId: e.configurationId, sectionId: e.id, sectionType: e.type)).toList(); visitAppContext.beaconSections = beaconSections; try { // Clear all before await DatabaseHelper.instance.clearTable(DatabaseTableType.beaconSection); // Store it locally for offline mode for(var beaconSection in beaconSections) { await DatabaseHelper.instance.insert(DatabaseTableType.beaconSection, ModelsHelper.beaconSectionToMap(beaconSection)); } print("STORE beaconSection DONE"); } catch(e) { print("Issue during beaconSection insertion"); print(e); } print("Got some Beacons for you"); print(beaconSections); appContext.setContext(visitAppContext); } } // Charge l'ApplicationInstance Mobile pour savoir si l'assistant/statistiques sont activés if (visitAppContext.applicationInstanceDTO == null && visitAppContext.instanceId != null) { try { final instances = await visitAppContext.clientAPI.applicationInstanceApi! .applicationInstanceGet(instanceId: visitAppContext.instanceId); final mobileInstance = instances?.where((e) => e.appType == AppType.Mobile).firstOrNull; if (mobileInstance != null) { visitAppContext.applicationInstanceDTO = mobileInstance; if (mobileInstance.isStatistic ?? false) { visitAppContext.statisticsService = StatisticsService( clientAPI: visitAppContext.clientAPI, instanceId: visitAppContext.instanceId, configurationId: visitAppContext.configuration?.id, appType: 'Mobile', language: visitAppContext.language, ); } } appContext.setContext(visitAppContext); } catch (e) { print("Could not load applicationInstance: $e"); } } return await ApiService.getConfigurations(visitAppContext.clientAPI, visitAppContext); } }