337 lines
13 KiB
Dart

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 = <ResourceType>[
...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<AppContext>(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,
),
],
),
),
],
);
}
}