manager-app/lib/Screens/Applications/app_configuration_link_screen.dart
2026-07-14 17:20:20 +02:00

949 lines
45 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 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<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 = 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<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);
}
},
);
}
),
),
),
// 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<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 ? '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<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;
}