manager-app/lib/Screens/Applications/app_configuration_link_screen.dart
Thomas Fransolet a6463acce6 Guide IA : libellés de canaux, activation depuis la carte, notifications topRight
La carte « Où le guide est disponible » affichait la valeur entière de l'enum
AppType (0, 1, 2…) au lieu d'un libellé. Réutilise les clés statsChannel*
déjà traduites en FR/EN/NL pour l'écran Statistiques.

L'activation de l'assistant par canal se réglait dans chaque onglet
d'application, alors que le champ vit sur ApplicationInstance et se résout
par (InstanceId, AppType). Le réglage passe dans la liste des canaux du
Guide IA, où tous les canaux sont visibles au même endroit ; le paramètre
showAssistant de AppConfigurationLinkScreen devient inutile.

Les notifications de cet écran passaient par ScaffoldMessenger (en bas),
contrairement au reste de l'application qui utilise showNotification
(toastification, topRight).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:30:33 +02:00

911 lines
42 KiB
Dart

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 String? configTitle;
final String? appUpdatedLabel;
AppConfigurationLinkScreen({Key? key, required this.applicationInstanceDTO, this.configTitle, this.appUpdatedLabel}) : super(key: key);
@override
_AppConfigurationLinkScreenState createState() => _AppConfigurationLinkScreenState();
}
class _AppConfigurationLinkScreenState extends State<AppConfigurationLinkScreen> {
late ApplicationInstanceDTO _applicationInstanceDTO;
final Map<String, Timer> _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<AppConfigurationLinkDTO> 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<AppContext>(context);
Size size = MediaQuery.of(context).size;
ManagerAppContext managerAppContext = appContext.getContext() as ManagerAppContext;
final appUpdatedMsg = widget.appUpdatedLabel ?? AppLocalizations.of(context)!.appUpdatedSuccess;
_generalInfoCard() {
const elementHeight = 116.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: 12,
runSpacing: 12,
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<String>.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<SectionEventDTO>? sectionEvents = rawSubsections?.whereType<SectionEventDTO>().toList();
sectionEvents = sectionEvents == null ? [] : sectionEvents;
sectionEvents.add(SectionEventDTO(id: null, label: AppLocalizations.of(context)!.noneOption));
return SingleChoiceInputContainer<SectionEventDTO?>(
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);
}
},
);
}
),
),
),
// L'activation de l'assistant par canal se règle dans l'onglet Guide IA,
// qui liste tous les canaux au même endroit.
],
),
),
);
}
)
)
],
),
),
);
}
_phoneConfigCard(
List<AppConfigurationLinkDTO>? 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<AppConfigurationLinkDTO>(
// 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<AppConfigurationLinkDTO>? 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 ? AppLocalizations.of(context)!.previewWebTitle : AppLocalizations.of(context)!.previewMobileTitle, 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(AppLocalizations.of(context)!.previewDesktop, !_webPreviewMobileWidth, () => setState(() => _webPreviewMobileWidth = false)),
_previewWidthChip(AppLocalizations.of(context)!.previewMobile, _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<dynamic> snapshot) {
List<AppConfigurationLinkDTO>? 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<void> updateMainInfos(DeviceDTO deviceToUpdate, dynamic appContext) async {
ManagerAppContext managerAppContext = appContext.getContext();
await managerAppContext.clientAPI!.deviceApi!.deviceUpdateMainInfos(deviceToUpdate);
}
Future<List<AppConfigurationLinkDTO>?> getAppConfigurationLink(AppContext appContext, ApplicationInstanceDTO applicationInstanceDTO) async {
try{
List<AppConfigurationLinkDTO>? 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<List<dynamic>?> 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<AppConfigurationLinkDTO?> addConfigurationToApp(AppContext appContext, AppConfigurationLinkDTO appConfigurationLink, ApplicationInstanceDTO applicationInstanceDTO) async {
AppConfigurationLinkDTO? appConfigurationsLink = await (appContext.getContext() as ManagerAppContext).clientAPI!.applicationInstanceApi!.applicationInstanceAddConfigurationToApplicationInstance(applicationInstanceDTO.id!, appConfigurationLink);
return appConfigurationsLink;
}
Future<ApplicationInstanceDTO?> 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<List<AppConfigurationLinkDTO>?> updateAppConfigurationOrder(AppContext appContext, List<AppConfigurationLinkDTO> appConfigurationLinks) async {
List<AppConfigurationLinkDTO>? appConfigurationsLinks = await (appContext.getContext() as ManagerAppContext).clientAPI!.applicationInstanceApi!.applicationInstanceUpdateApplicationLinkOrder(appConfigurationLinks);
return appConfigurationsLinks;
}
Future<AppConfigurationLinkDTO?> updateApplicationLink(AppContext appContext, AppConfigurationLinkDTO appConfigurationLinkDTO) async {
AppConfigurationLinkDTO? result = await (appContext.getContext() as ManagerAppContext).clientAPI!.applicationInstanceApi!.applicationInstanceUpdateApplicationLink(appConfigurationLinkDTO);
return result;
}
Future<String?> deleteConfigurationToApp(AppContext appContext, AppConfigurationLinkDTO appConfigurationLink, ApplicationInstanceDTO applicationInstanceDTO) async {
var result = await (appContext.getContext() as ManagerAppContext).clientAPI!.applicationInstanceApi!.applicationInstanceDeleteAppConfigurationLink(applicationInstanceDTO.id!, appConfigurationLink.id!);
return result;
}