import 'package:flutter/material.dart'; import 'package:manager_api_new/api.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_app/Components/geometry_input_container.dart'; import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/number_stepper_field.dart'; import 'package:manager_app/Components/section_card.dart'; import 'package:manager_app/Screens/Configurations/Section/SubSection/Slider/listView_card_image.dart'; import 'package:manager_app/Screens/Configurations/Section/SubSection/Slider/new_update_image_slider.dart'; import 'package:provider/provider.dart'; import 'question_fields.dart'; /// Médias acceptés sur une étape de parcours. Le PDF s'ajoute aux types du /// slider : une étape peut porter un plan, une partition, un fac-similé. /// Le rendu visiteur correspondant vit dans `CachedCustomResource` /// (mymuseum-visitapp) et `ResourceViewer.tsx` (visitapp-web). const kGuidedStepResourceTypes = [ ...kSliderContentResourceTypes, ResourceType.Pdf, ]; /// Les champs d'une étape, sans son contenant : ils s'affichent dans le /// panneau de droite de l'éditeur de parcours, à côté du rail. /// /// Le DTO est modifié en place ; `onChanged` prévient l'hôte. class EtapeFields extends StatelessWidget { const EtapeFields({ Key? key, required this.step, required this.onChanged, required this.isEscapeMode, this.isGeolocated = true, }) : super(key: key); final GuidedStepDTO step; final VoidCallback onChanged; final bool isEscapeMode; final bool isGeolocated; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final appCtx = Provider.of(context, listen: false); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: MultiStringInputContainer( label: l10n.titleColonLabel, modalLabel: l10n.stepTitleLabel, initialValue: step.title ?? [], onGetResult: (val) { step.title = val; onChanged(); }, maxLines: 1, isTitle: true, isHTML: true, showPreview: true, ), ), SizedBox(width: kSpace7), Expanded( child: MultiStringInputContainer( label: l10n.descriptionColonLabel, modalLabel: l10n.stepDescriptionLabel, initialValue: step.description ?? [], onGetResult: (val) { step.description = val; onChanged(); }, maxLines: 1, isTitle: false, isHTML: true, showPreview: true, ), ), ], ), SizedBox(height: kSpace5), // Masqué pour un parcours en salle (ShowMap = false) : sans carte, une // position et une zone de déclenchement n'ont aucun effet. if (isGeolocated) ...[ SectionCard( icon: Icons.location_on, title: l10n.stepLocationTitle, subtitle: l10n.stepLocationSub, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GeometryInputContainer( label: l10n.stepLocationLabel, initialGeometry: step.geometry, initialColor: null, // GuidedStepDTO n'a pas de champ color (contrairement à // MapAnnotationDTO.polyColor) : bouton masqué plutôt que // supprimé, ne pas retirer comme du code mort. showColorButton: false, onSave: (geometry, color) { step.geometry = geometry; onChanged(); }, ), SizedBox(height: kSpace3), Row( children: [ Expanded( child: Row( children: [ Switch( value: step.isGeoTriggered ?? false, onChanged: (val) { step.isGeoTriggered = val; if (val != true) step.zoneRadiusMeters = null; onChanged(); }, activeThumbColor: kPrimaryColor, ), SizedBox(width: kSpace3), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(l10n.stepGeoTrigger, style: kTextBody), Text( l10n.stepGeoTriggerDesc, style: kTextHint, ), ], ), ), ], ), ), if (step.isGeoTriggered == true) Padding( padding: const EdgeInsets.only(left: kSpace4), child: NumberStepperField( label: l10n.stepRadiusLabel, value: step.zoneRadiusMeters, min: 1, max: 500, unit: l10n.metersAbbr, onChanged: (val) { step.zoneRadiusMeters = val.toDouble(); onChanged(); }, ), ), ], ), ], ), ), SizedBox(height: kSpace5), ], SectionCard( icon: Icons.perm_media, title: l10n.stepRichContentTitle, subtitle: l10n.stepRichContentSub, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MultiStringInputContainer( label: l10n.audioColonLabel, resourceTypes: [ResourceType.Audio], modalLabel: l10n.stepGuideAudioLabel, initialValue: step.audioIds ?? [], onGetResult: (val) { step.audioIds = val.isEmpty ? null : val; onChanged(); }, maxLines: 1, isTitle: false, isHTML: false, ), SizedBox(height: kSpace4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.mediasColonLabel, style: kLabelField), IconButton( icon: Icon(Icons.add_circle_outline, color: kSuccess), onPressed: () async { final result = await showNewOrUpdateContentSlider( null, appCtx, context, true, false, resourceTypes: kGuidedStepResourceTypes, ); if (result == null) return; result.order = step.contents?.length ?? 0; step.contents = [...(step.contents ?? []), result]; onChanged(); }, ), ], ), if (step.contents?.isEmpty ?? true) Padding( padding: const EdgeInsets.only(bottom: kSpace3), child: Text( l10n.stepNoMedia, style: kTextHint.copyWith(fontStyle: FontStyle.italic), ), ) else SizedBox( height: 250, child: ReorderableListView( scrollDirection: Axis.horizontal, onReorderItem: (oldIndex, newIndex) { final item = step.contents!.removeAt(oldIndex); step.contents!.insert(newIndex, item); for (var i = 0; i < step.contents!.length; i++) { step.contents![i].order = i; } onChanged(); }, children: List.generate( step.contents!.length, (i) => ListViewCardContent( step.contents!, i, Key('content_$i'), appCtx, (updated) { step.contents = List.from(updated); onChanged(); }, true, false, ), ), ), ), ], ), ), SizedBox(height: kSpace5), // Questions — disponibles pour tous les parcours (guidés et escape game) SectionCard( icon: Icons.quiz, title: l10n.questionsChallengesLabel, subtitle: isEscapeMode ? l10n.stepRiddleSub : l10n.stepQuizSub, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Row( children: [ Switch( value: step.isStepTimer ?? false, onChanged: (val) { step.isStepTimer = val; onChanged(); }, activeThumbColor: kPrimaryColor, ), SizedBox(width: kSpace3), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(l10n.stepTimerTitle, style: kTextBody), Text( l10n.stepTimerDesc, style: kTextHint, ), ], ), ), ], ), ), if (step.isStepTimer == true) Padding( padding: const EdgeInsets.only(left: kSpace4), child: NumberStepperField( label: l10n.durationSecondsLabel, value: step.timerSeconds, min: 0, max: 3600, unit: l10n.secondsAbbr, onChanged: (val) { step.timerSeconds = val.toInt(); onChanged(); }, ), ), ], ), if (step.isStepTimer == true) ...[ SizedBox(height: kSpace3), MultiStringInputContainer( label: l10n.stepTimerExpiredLabel, modalLabel: l10n.stepTimerExpiredModalLabel, initialValue: step.timerExpiredMessage ?? [], onGetResult: (val) { step.timerExpiredMessage = val.isEmpty ? null : val; onChanged(); }, maxLines: 2, isTitle: false, isHTML: true, showPreview: true, ), SizedBox(height: kSpace3), ], QuestionsBlock( questions: step.quizQuestions ??= [], stepId: step.id ?? "temp", onChanged: onChanged, ), ], ), ), ], ); } }