import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:manager_app/Components/color_picker_input_container.dart'; import 'package:manager_app/Components/common_loader.dart'; import 'package:manager_app/Components/confirmation_dialog.dart'; import 'package:manager_app/Components/message_notification.dart'; import 'package:manager_app/Components/multi_select_dropdown_language_container.dart'; import 'package:manager_app/Components/card_shape_selector.dart'; import 'package:manager_app/Components/reorderable_custom_list.dart'; import 'package:manager_app/Components/resource_input_container.dart'; import 'package:manager_app/Components/single_choice_input_container.dart'; import 'package:manager_app/Models/managerContext.dart'; import 'package:manager_app/Screens/Applications/add_configuration_link_popup.dart'; import 'package:manager_app/Screens/Applications/bento_preview_grid.dart'; import 'package:manager_app/Screens/Applications/browser_frame.dart'; import 'package:manager_app/Screens/Applications/phone_mockup.dart'; import 'package:manager_app/app_context.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_app/l10n/app_localizations.dart'; import 'package:manager_api_new/api.dart'; import 'package:provider/provider.dart'; class AppConfigurationLinkScreen extends StatefulWidget { final ApplicationInstanceDTO applicationInstanceDTO; final bool showAssistant; final String? configTitle; final String? appUpdatedLabel; AppConfigurationLinkScreen({Key? key, required this.applicationInstanceDTO, this.showAssistant = true, this.configTitle, this.appUpdatedLabel}) : super(key: key); @override _AppConfigurationLinkScreenState createState() => _AppConfigurationLinkScreenState(); } class _AppConfigurationLinkScreenState extends State { late ApplicationInstanceDTO _applicationInstanceDTO; final Map _spanDebounceTimers = {}; // Largeur de viewport simulée pour l'aperçu Web (le site est responsive, // pas figé — l'admin doit pouvoir vérifier les deux). bool _webPreviewMobileWidth = false; @override void initState() { super.initState(); _applicationInstanceDTO = widget.applicationInstanceDTO; } @override void dispose() { for (final timer in _spanDebounceTimers.values) { timer.cancel(); } super.dispose(); } // Met à jour les spans localement (sélecteur de forme et aperçu restent // synchronisés sur le même objet link), puis débounce l'appel réseau pendant // un glisser de coin continu. void _onSpanChanged( AppConfigurationLinkDTO link, int colSpan, int rowSpan, AppContext appContext, BuildContext context, String appUpdatedMsg, void Function(void Function()) localSetState, ) { link.gridColSpan = colSpan; link.gridRowSpan = rowSpan; localSetState(() {}); _spanDebounceTimers[link.id!]?.cancel(); // Débounce le temps du glisser ; la notif n'arrive qu'une fois la // sauvegarde réellement effectuée, pas à chaque pixel de resize. _spanDebounceTimers[link.id!] = Timer(const Duration(milliseconds: 400), () async { try { await updateApplicationLink(appContext, link); if (context.mounted) showNotification(kSuccess, kWhite, appUpdatedMsg, context, null); } catch (e) { if (context.mounted) { showNotification(kError, kWhite, AppLocalizations.of(context)!.errorOccurred, context, null); } } }); } // Glisser une card sur une autre dans l'aperçu échange leur position dans la // liste (donc leur `order`) ; le dense packing recalcule le reste tout seul. // La liste de gauche se resynchronise via sa clé (basée sur la séquence des // ids), puisqu'elle travaille sur sa propre copie interne de `links`. void _onSwap( String fromId, String toId, List appConfigurationLinks, AppContext appContext, BuildContext context, String appUpdatedMsg, void Function(void Function()) localSetState, ) async { final fromIndex = appConfigurationLinks.indexWhere((l) => l.id == fromId); final toIndex = appConfigurationLinks.indexWhere((l) => l.id == toId); if (fromIndex == -1 || toIndex == -1) return; final tmp = appConfigurationLinks[fromIndex]; appConfigurationLinks[fromIndex] = appConfigurationLinks[toIndex]; appConfigurationLinks[toIndex] = tmp; int order = 0; for (final item in appConfigurationLinks) { item.order = order; order++; } localSetState(() {}); try { await updateAppConfigurationOrder(appContext, appConfigurationLinks); showNotification(kSuccess, kWhite, appUpdatedMsg, context, null); } catch (e) { showNotification(kError, kWhite, AppLocalizations.of(context)!.errorOccurred, context, null); } } @override Widget build(BuildContext context) { final appContext = Provider.of(context); Size size = MediaQuery.of(context).size; ManagerAppContext managerAppContext = appContext.getContext() as ManagerAppContext; final appUpdatedMsg = widget.appUpdatedLabel ?? AppLocalizations.of(context)!.appUpdatedSuccess; _generalInfoCard() { const elementHeight = 125.0; return Card( margin: const EdgeInsets.symmetric(vertical: 8), color: kWhite, elevation: 0, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(AppLocalizations.of(context)!.generalInfo, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 21)), SizedBox(height: 8), Expanded( child: LayoutBuilder( builder: (context, constraints) { final elementWidth = (constraints.maxWidth < 420) ? constraints.maxWidth : 400.0; return Center( child: SingleChildScrollView( child: Wrap( alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, runAlignment: WrapAlignment.center, spacing: 16, runSpacing: 16, children: [ // Image principale Container( width: elementWidth, height: elementHeight, child: Center( child: ResourceInputContainer( label: AppLocalizations.of(context)!.mainImageLabel, initialValue: _applicationInstanceDTO.mainImageId, color: kPrimaryColor, imageFit: BoxFit.fitHeight, onChanged: (ResourceDTO resource) async { if(resource.id == null) { _applicationInstanceDTO.mainImageId = null; _applicationInstanceDTO.mainImageUrl = null; } else { _applicationInstanceDTO.mainImageId = resource.id; _applicationInstanceDTO.mainImageUrl = resource.url; } // automatic save var applicationLink = await updateApplicationInstance(appContext, _applicationInstanceDTO); if(applicationLink != null) { showNotification(kSuccess, kWhite, appUpdatedMsg, context, null); /*setState(() { });*/ } }, ), ), ), // Image Loader Container( width: elementWidth, height: elementHeight, child: Center( child: ResourceInputContainer( label: AppLocalizations.of(context)!.loaderLabel, initialValue: _applicationInstanceDTO.loaderImageId, color: kPrimaryColor, imageFit: BoxFit.fitHeight, onChanged: (ResourceDTO resource) async { if(resource.id == null) { _applicationInstanceDTO.loaderImageId = null; _applicationInstanceDTO.loaderImageUrl = null; } else { _applicationInstanceDTO.loaderImageId = resource.id; _applicationInstanceDTO.loaderImageUrl = resource.url; } // automatic save var applicationLink = await updateApplicationInstance(appContext, _applicationInstanceDTO); if(applicationLink != null) { showNotification(kSuccess, kWhite, appUpdatedMsg, context, null); /*setState(() { });*/ } }, ), ), ), // Primary color Container( width: elementWidth, height: elementHeight, child: Center( child: ColorPickerInputContainer( label: AppLocalizations.of(context)!.primaryColorLabel, fontSize: 20, color: _applicationInstanceDTO.primaryColor, onChanged: (value) async { _applicationInstanceDTO.primaryColor = value; // automatic save var applicationLink = await updateApplicationInstance(appContext, _applicationInstanceDTO); if(applicationLink != null) { showNotification(kSuccess, kWhite, appUpdatedMsg, context, null); /*setState(() { });*/ } }, ), ), ), // Secondary color SizedBox( width: elementWidth, height: elementHeight, child: Center( child: ColorPickerInputContainer( label: AppLocalizations.of(context)!.secondaryColorLabel, fontSize: 20, color: _applicationInstanceDTO.secondaryColor, onChanged: (value) async { _applicationInstanceDTO.secondaryColor = value; // automatic save var applicationLink = await updateApplicationInstance(appContext, _applicationInstanceDTO); if(applicationLink != null) { showNotification(kSuccess, kWhite, appUpdatedMsg, context, null); /*setState(() { });*/ } }, ), ), ), // Langues SizedBox( width: elementWidth, height: elementHeight, child: Center( child: MultiSelectDropdownLanguageContainer( label: AppLocalizations.of(context)!.languagesLabel, initialValue: _applicationInstanceDTO.languages != null ? _applicationInstanceDTO.languages!: [], values: languages, isMultiple: true, fontSize: 20, isAtLeastOne: true, onChanged: (value) async { var tempOutput = new List.from(value); _applicationInstanceDTO.languages = tempOutput; // automatic save var applicationLink = await updateApplicationInstance(appContext, _applicationInstanceDTO); if(applicationLink != null) { showNotification(kSuccess, kWhite, appUpdatedMsg, context, null); /*setState(() { });*/ } //print(configurationDTO.languages); }, ), ), ), // Highlight / Event principal Container( width: elementWidth, height: elementHeight, child: Center( child: FutureBuilder( future: getSectionEvents(appContext, _applicationInstanceDTO), builder: (context, snapshot) { var rawList = snapshot.data; var rawSubsections = jsonDecode(jsonEncode(snapshot.data)); rawSubsections = rawSubsections?.map((json) => SectionEventDTO.fromJson(json)).toList(); List? sectionEvents = rawSubsections?.whereType().toList(); sectionEvents = sectionEvents == null ? [] : sectionEvents; sectionEvents.add(SectionEventDTO(id: null, label: AppLocalizations.of(context)!.noneOption)); return SingleChoiceInputContainer( label: AppLocalizations.of(context)!.featuredEventLabel, selectLabel: AppLocalizations.of(context)!.chooseEvent, selected: _applicationInstanceDTO.sectionEventDTO, values: sectionEvents.toList(), valueExtractor: (SectionEventDTO? dto) => dto?.id ?? "", labelExtractor: (SectionEventDTO? dto) => dto?.label ?? "Aucun", onChanged: (SectionEventDTO? sectionEvent) async { // "Aucun" est un DTO sentinelle (id: null), pas un vrai // null Dart : sectionEvent?.id couvre les deux cas. _applicationInstanceDTO.sectionEventId = sectionEvent?.id; var applicationLink = await updateApplicationInstance(appContext, _applicationInstanceDTO); if(applicationLink != null) { // setState (pas localSetState) : l'aperçu (_previewHero) // lit _applicationInstanceDTO en dehors du StatefulBuilder // de la liste/aperçu, il faut rebuild tout l'écran. setState(() { _applicationInstanceDTO.sectionEventDTO = applicationLink.sectionEventDTO; }); showNotification(kSuccess, kWhite, appUpdatedMsg, context, null); } }, ); } ), ), ), // Assistant IA — visible uniquement si l'instance a la fonctionnalité if (widget.showAssistant && managerAppContext.instanceDTO?.isAssistant == true) Container( width: elementWidth, height: elementHeight, child: Center( child: StatefulBuilder( builder: (context, localSetState) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(AppLocalizations.of(context)!.aiAssistantLabel, style: TextStyle(fontSize: 16)), Switch( activeThumbColor: kPrimaryColor, inactiveThumbColor: kBodyTextColor, inactiveTrackColor: kSecond, hoverColor: kPrimaryColor.withValues(alpha: 0.2), value: _applicationInstanceDTO.isAssistant ?? false, onChanged: (bool newValue) async { try { _applicationInstanceDTO.isAssistant = newValue; var applicationLink = await updateApplicationInstance(appContext, _applicationInstanceDTO); if (applicationLink != null) { localSetState(() { _applicationInstanceDTO.isAssistant = applicationLink.isAssistant; }); showNotification(kSuccess, kWhite, appUpdatedMsg, context, null); } } catch (e) { showNotification(kError, kWhite, AppLocalizations.of(context)!.errorOccurred, context, null); } }, ), ], ); }, ), ), ), ], ), ), ); } ) ) ], ), ), ); } _phoneConfigCard( List? appConfigurationLinks, void Function(void Function()) localSetState, ) { return Card( margin: const EdgeInsets.symmetric(vertical: 8), color: kWhite, elevation: 0, child: Stack( //crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(16), child: Text(widget.configTitle ?? AppLocalizations.of(context)!.phoneConfigTitle, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 21)), ), appConfigurationLinks != null ? Padding( padding: const EdgeInsets.only(left: 32, right: 32, top: 75), child: Container( height: size.height * 0.6, width: size.width * 0.9, constraints: BoxConstraints( minHeight: 300, minWidth: 300, maxHeight: 500 ), child: SingleChildScrollView( child: ReorderableCustomList( // Basé sur la séquence des ids (pas juste la référence de la // liste, mutée en place par le swap dans l'aperçu) pour que // la liste se resynchronise dès que l'ordre change ailleurs. key: ValueKey(appConfigurationLinks.map((l) => l.id).join(',')), items: appConfigurationLinks, shrinkWrap: true, onChanged: (updatedList) async { int order = 0; // update order manually for(var item in updatedList) { item.order = order; order++; } // TODO use order put method var result = await updateAppConfigurationOrder(appContext, updatedList); localSetState(() {}); showNotification(kSuccess, kWhite, appUpdatedMsg, context, null); }, actions: [ (BuildContext context, int index, AppConfigurationLinkDTO link) { return Container( height: 50, width: 70, child: Switch( activeThumbColor: kPrimaryColor, inactiveThumbColor: kBodyTextColor, inactiveTrackColor: kSecond, hoverColor: kPrimaryColor.withValues(alpha: 0.2), value: link.isActive ?? false, onChanged: (bool newValue) async { try { link.isActive = newValue; var applicationLink = await updateApplicationLink(appContext, link); if(applicationLink != null) { if(newValue) { showNotification(kSuccess, kWhite, AppLocalizations.of(context)!.configActivatedSuccess, context, null); } else { showNotification(kSuccess, kWhite, AppLocalizations.of(context)!.configDeactivatedSuccess, context, null); } localSetState(() { link.isActive = applicationLink.isActive; }); } } catch (e) { showNotification(kError, kWhite, AppLocalizations.of(context)!.errorOccurred, context, null); } }, ), ); }, if (_applicationInstanceDTO.appType != AppType.Tablet) (BuildContext context, int index, AppConfigurationLinkDTO link) { return Container( height: 50, width: 130, alignment: Alignment.center, child: CardShapeSelector( colSpan: (link.gridColSpan ?? 1).clamp(1, 2), rowSpan: (link.gridRowSpan ?? 1).clamp(1, 2), onChanged: (col, row) { _onSpanChanged(link, col, row, appContext, context, appUpdatedMsg, localSetState); }, ), ); }, (BuildContext context, int index, AppConfigurationLinkDTO link) { return Container( height: 50, width: 50, child: InkWell( onTap: () async { showConfirmationDialog( AppLocalizations.of(context)!.configRemoveConfirm, () {}, () async { try { var result = await deleteConfigurationToApp(appContext, link, _applicationInstanceDTO); showNotification(kSuccess, kWhite, AppLocalizations.of(context)!.configRemovedSuccess, context, null); setState(() { // for refresh ui }); } catch(e) { showNotification(kError, kWhite, AppLocalizations.of(context)!.configRemoveError, context, null); } }, context ); }, child: Icon(Icons.delete, color: kError, size: 25), ), ); }, ], padding: const EdgeInsets.all(8), itemBuilder: (context, index, appConfigurationLink) { return Container( key: ValueKey(appConfigurationLink.id), decoration: BoxDecoration(), margin: const EdgeInsets.symmetric(vertical: 3), padding: const EdgeInsets.all(8), child: Row( children: [ if(appConfigurationLink.configuration!.imageId != null) Container( width: 50, height: 50, decoration: BoxDecoration( color: kSecond.withValues(alpha: 0.65), borderRadius: BorderRadius.circular(8.0), image: DecorationImage( fit: BoxFit.cover, image: NetworkImage(appConfigurationLink.configuration!.imageSource!) ), ) ), Padding( padding: const EdgeInsets.all(8.0), child: Text(appConfigurationLink.configuration?.label ?? ""), ), ], ), ); }, ), ), ), ): Center(child: Text(AppLocalizations.of(context)!.noData)), appConfigurationLinks != null ? Positioned( top: 8, right: 8, child: InkWell( onTap: () async { // Show configuration selector to link with ! var result = await showAddConfigurationLink(context, appContext, managerAppContext.instanceDTO!, appConfigurationLinks.map((acl) => acl.configurationId!).toList() ?? []); if(result != null) { for(var configurationId in result) { AppConfigurationLinkDTO appConfigurationLinkDTO = AppConfigurationLinkDTO( applicationInstanceId: _applicationInstanceDTO.id, configurationId: configurationId, isActive: true, isDate: false, isHour: false, isSectionImageBackground: false ); await addConfigurationToApp(appContext, appConfigurationLinkDTO, _applicationInstanceDTO); } setState(() { // Refresh ui }); } }, child: Container( height: 60, width: 60, decoration: BoxDecoration( color: kSuccess, borderRadius: BorderRadius.circular(12.0), ), child: Padding( padding: const EdgeInsets.all(8.0), child: Icon(Icons.add, size: 24, color: kWhite), ) ), ), ) : SizedBox(), ], ), ); } // Reproduit le header de la vraie home (event en vedette, sinon image // principale de l'app) — pour que l'aperçu soit fidèle à ce que le // visiteur voit réellement, pas juste la grille de sections. // [heroHeight] est fixe (indépendant du nombre d'items du bento en dessous) // pour matcher le vrai comportement : SliverAppBar.expandedHeight figé côté // mymuseum-visitapp, `height: 50vh` figé côté HomeHero.tsx (visitapp-web). _previewHero(double heroHeight) { final event = _applicationInstanceDTO.sectionEventDTO; final imageUrl = event?.imageSource ?? _applicationInstanceDTO.mainImageUrl; final title = (event?.title?.isNotEmpty ?? false) ? event!.title!.first.value : null; if (imageUrl == null && title == null) return const SizedBox(); return Container( height: heroHeight, margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: kSecond.withValues(alpha: 0.4), image: imageUrl != null ? DecorationImage(image: NetworkImage(imageUrl), fit: BoxFit.cover) : null, ), child: Stack( children: [ Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.transparent, Colors.black.withValues(alpha: 0.75)], stops: const [0.4, 1.0], ), ), ), ), if (title != null) Positioned( bottom: 10, left: 12, right: 12, // Titre en HTML (contenu Quill) — hauteur bornée + clip pour // ne jamais déborder, même si le moteur ignore line-clamp. child: ClipRect( child: SizedBox( height: 20, child: HtmlWidget( title, textStyle: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w700), customStylesBuilder: (_) => {'font-family': 'Roboto'}, ), ), ), ), ], ), ); } Widget _previewWidthChip(String label, bool isSelected, VoidCallback onTap) { return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 150), margin: const EdgeInsets.symmetric(horizontal: 1), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: isSelected ? kPrimaryColor : Colors.transparent, borderRadius: BorderRadius.circular(6), ), child: Text( label, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: isSelected ? kWhite : kBodyTextColor, ), ), ), ); } // Panneau d'aperçu — Card distincte, séparée de la liste (pas de conflit // avec le bouton "+" qui reste positionné en absolu dans _phoneConfigCard). _previewCard( List? appConfigurationLinks, void Function(void Function()) localSetState, ) { if (appConfigurationLinks == null || _applicationInstanceDTO.appType == AppType.Tablet) { return const SizedBox(); } final isWeb = _applicationInstanceDTO.appType == AppType.Web; // Nombre de colonnes de l'aperçu = celui de la cible réelle : mobile & web // mobile = 2, web desktop = 4 (breakpoint lg de ConfigurationGrid.tsx). final previewColumns = isWeb ? (_webPreviewMobileWidth ? 2 : 4) : 2; // visitapp-web est un site responsive, pas figé à une largeur — on doit // pouvoir vérifier le rendu desktop ET mobile web, pas juste une seule // largeur de cadre arbitraire. const mobileWebWidth = 390.0; // Ratio largeur/hauteur approximatif d'un smartphone (~9:19.5). const phoneAspect = 0.46; // Largeur de référence pour dériver le hero mobile à l'échelle du cadre // (voir realMobileHeroHeight ci-dessous). const referencePhoneWidth = mobileWebWidth; // Hauteur réelle du hero sur mymuseum-visitapp : SliverAppBar.expandedHeight // fixe (lib/Screens/Home/home_3.0.dart), indépendante de la taille d'écran // et du nombre d'items du bento en dessous — donc on la met à l'échelle du // cadre au lieu de la recalculer depuis la hauteur dispo. const realMobileHeroHeight = 235.0; // Le hero garde une taille fixe (fidèle à la vraie app, cf. commentaire de // _previewHero), mais scrolle AVEC le reste du contenu — le vrai // SliverAppBar est `pinned: false` côté mymuseum-visitapp (home_3.0.dart), // il disparaît donc au scroll, ce n'est pas un header épinglé. Widget buildPreviewContent(double frameWidth, double frameHeight, bool isWebFrame) { final heroHeight = isWebFrame ? (frameHeight * 0.5).clamp(110.0, 320.0) : realMobileHeroHeight * (frameWidth / referencePhoneWidth); return SingleChildScrollView( padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _previewHero(heroHeight), BentoPreviewGrid( links: appConfigurationLinks, columns: previewColumns, onSpanChanged: (link, col, row) => _onSpanChanged( link, col, row, appContext, context, appUpdatedMsg, localSetState), onSwap: (fromId, toId) => _onSwap( fromId, toId, appConfigurationLinks, appContext, context, appUpdatedMsg, localSetState), ), ], ), ); } return Card( margin: const EdgeInsets.symmetric(vertical: 8), color: kWhite, elevation: 0, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text(isWeb ? 'Aperçu — Web' : 'Aperçu — Mobile', style: TextStyle(fontWeight: FontWeight.w500, fontSize: 21)), const Spacer(), if (isWeb) Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: kSecond.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ _previewWidthChip('Desktop', !_webPreviewMobileWidth, () => setState(() => _webPreviewMobileWidth = false)), _previewWidthChip('Mobile', _webPreviewMobileWidth, () => setState(() => _webPreviewMobileWidth = true)), ], ), ), ], ), const SizedBox(height: 16), // Le cadre est dimensionné sur l'espace réellement disponible ici // (pas sur tout l'écran) pour qu'il soit toujours visible en // entier, sans avoir à scroller pour voir le bas du smartphone. Expanded( child: LayoutBuilder( builder: (context, frameConstraints) { final availableHeight = frameConstraints.maxHeight; final availableWidth = frameConstraints.maxWidth; // Le mode "Mobile" de l'onglet Web simule un visiteur qui // consulte le site sur son téléphone : même silhouette de // téléphone que l'onglet Mobile, mais avec le contenu/hero // rendu comme visitapp-web (50vh, colonnes web-mobile). final useDesktopBrowser = isWeb && !_webPreviewMobileWidth; final Widget frame; if (useDesktopBrowser) { frame = BrowserFrame( width: availableWidth, height: availableHeight, child: buildPreviewContent(availableWidth, availableHeight, true), ); } else { var phoneHeight = availableHeight; var phoneWidth = phoneHeight * phoneAspect; if (phoneWidth > availableWidth) { phoneWidth = availableWidth; phoneHeight = phoneWidth / phoneAspect; } frame = PhoneMockup( width: phoneWidth, height: phoneHeight, child: buildPreviewContent(phoneWidth, phoneHeight, isWeb), ); } return Align(alignment: Alignment.topCenter, child: frame); }, ), ), ], ), ), ); } return FutureBuilder( future: getAppConfigurationLink(appContext, _applicationInstanceDTO), builder: (context, AsyncSnapshot snapshot) { List? appConfigurationLinks = snapshot.data; if (snapshot.connectionState == ConnectionState.done) { return LayoutBuilder( builder: (context, constraints) { final maxHeight = constraints.maxHeight; return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.start, children: [ ConstrainedBox( constraints: BoxConstraints( maxHeight: maxHeight * 0.42, minHeight: 150, ), child: _generalInfoCard(), ), const SizedBox(height: 8), ConstrainedBox( constraints: BoxConstraints( maxHeight: maxHeight * 0.53, minHeight: 150, ), // Un seul StatefulBuilder partagé entre la liste et l'aperçu : // les deux Card restent synchronisées sur le même état de poids. child: StatefulBuilder( builder: (context, localSetState) { final showPreview = _applicationInstanceDTO.appType != AppType.Tablet; return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( flex: showPreview ? 3 : 1, child: _phoneConfigCard(appConfigurationLinks, localSetState), ), if (showPreview) ...[ const SizedBox(width: 8), Expanded( flex: 2, child: _previewCard(appConfigurationLinks, localSetState), ), ], ], ); }, ), ), ], ), ); }, ); } else if (snapshot.connectionState == ConnectionState.none) { return Text(AppLocalizations.of(context)!.noData); } else { return Center( child: Container( height: size.height * 0.2, child: CommonLoader() ) ); } } ); } } Future updateMainInfos(DeviceDTO deviceToUpdate, dynamic appContext) async { ManagerAppContext managerAppContext = appContext.getContext(); await managerAppContext.clientAPI!.deviceApi!.deviceUpdateMainInfos(deviceToUpdate); } Future?> getAppConfigurationLink(AppContext appContext, ApplicationInstanceDTO applicationInstanceDTO) async { try{ List? appConfigurationsLinks = await (appContext.getContext() as ManagerAppContext).clientAPI!.applicationInstanceApi!.applicationInstanceGetAllApplicationLinkFromApplicationInstance(applicationInstanceDTO.id!); if(appConfigurationsLinks != null) { appConfigurationsLinks.forEach((element) { print(element); }); } return appConfigurationsLinks; } catch(e) { return null; } } Future?> getSectionEvents(AppContext appContext, ApplicationInstanceDTO applicationInstanceDTO) async { try{ final rawList = await (appContext.getContext() as ManagerAppContext).clientAPI!.sectionApi!.sectionGetAllFromType(instanceId: applicationInstanceDTO.instanceId!, sectionType: SectionType.Event.toString()); if(rawList == null) { return [];} var sections = rawList.map((json) => SectionEventDTO.fromJson(json)!).toList(); return sections ?? []; } catch(e) { return null; } } Future addConfigurationToApp(AppContext appContext, AppConfigurationLinkDTO appConfigurationLink, ApplicationInstanceDTO applicationInstanceDTO) async { AppConfigurationLinkDTO? appConfigurationsLink = await (appContext.getContext() as ManagerAppContext).clientAPI!.applicationInstanceApi!.applicationInstanceAddConfigurationToApplicationInstance(applicationInstanceDTO.id!, appConfigurationLink); return appConfigurationsLink; } Future updateApplicationInstance(AppContext appContext, ApplicationInstanceDTO applicationInstanceDTO) async { var applicationInstanceToSend = ApplicationInstanceDTO.fromJson( applicationInstanceDTO.toJson() ); applicationInstanceToSend?.sectionEventDTO = null; applicationInstanceToSend?.sectionEventId = applicationInstanceDTO.sectionEventId; applicationInstanceToSend?.appType = applicationInstanceDTO.appType; ApplicationInstanceDTO? result = await (appContext.getContext() as ManagerAppContext).clientAPI!.applicationInstanceApi!.applicationInstanceUpdate(applicationInstanceToSend!); return result; } Future?> updateAppConfigurationOrder(AppContext appContext, List appConfigurationLinks) async { List? appConfigurationsLinks = await (appContext.getContext() as ManagerAppContext).clientAPI!.applicationInstanceApi!.applicationInstanceUpdateApplicationLinkOrder(appConfigurationLinks); return appConfigurationsLinks; } Future updateApplicationLink(AppContext appContext, AppConfigurationLinkDTO appConfigurationLinkDTO) async { AppConfigurationLinkDTO? result = await (appContext.getContext() as ManagerAppContext).clientAPI!.applicationInstanceApi!.applicationInstanceUpdateApplicationLink(appConfigurationLinkDTO); return result; } Future deleteConfigurationToApp(AppContext appContext, AppConfigurationLinkDTO appConfigurationLink, ApplicationInstanceDTO applicationInstanceDTO) async { var result = await (appContext.getContext() as ManagerAppContext).clientAPI!.applicationInstanceApi!.applicationInstanceDeleteAppConfigurationLink(applicationInstanceDTO.id!, appConfigurationLink.id!); return result; }