Réglages nom d'app / scan QR / liens stores, et nouvelle URL de QR

Écran d'une application : nom de l'application (FR/EN/NL), case
d'affichage du scan QR, et liens stores réservés au SuperAdmin.
MultiStringInputContainer accepte des langues explicites, faute de
configuration sélectionnée sur cet écran.

Le QR d'une section pointe vers app.myinfomate.be : la page /download
si l'instance a une app mobile, la section web directement sinon.
web.mymuseum.be était désuet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-09-10 11:42:15 +02:00
parent a1701e216a
commit 0c3bc31b5e
12 changed files with 263 additions and 6 deletions

View File

@ -22,6 +22,9 @@ class MultiStringInputContainer extends StatelessWidget {
final double fontSize; final double fontSize;
final bool isMandatory; final bool isMandatory;
final bool showPreview; final bool showPreview;
/// Hors d'une configuration (ex. réglages d'une application), les langues ne
/// peuvent pas venir de `selectedConfiguration` : on les passe explicitement.
final List<String>? languages;
const MultiStringInputContainer({ const MultiStringInputContainer({
Key? key, Key? key,
@ -37,6 +40,7 @@ class MultiStringInputContainer extends StatelessWidget {
this.fontSize = 18, this.fontSize = 18,
this.isMandatory = true, this.isMandatory = true,
this.showPreview = false, this.showPreview = false,
this.languages,
}) : super(key: key); }) : super(key: key);
String _plainTextPreview(String htmlOrText) { String _plainTextPreview(String htmlOrText) {
@ -55,7 +59,7 @@ class MultiStringInputContainer extends StatelessWidget {
final appContext = Provider.of<AppContext>(context); final appContext = Provider.of<AppContext>(context);
final managerAppContext = appContext.getContext(); final managerAppContext = appContext.getContext();
final languages = managerAppContext.selectedConfiguration!.languages!; final languages = this.languages ?? managerAppContext.selectedConfiguration!.languages!;
final completed = final completed =
initialValue.where((t) => (t.value ?? "").trim().isNotEmpty).length; initialValue.where((t) => (t.value ?? "").trim().isNotEmpty).length;
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
@ -85,7 +89,7 @@ class MultiStringInputContainer extends StatelessWidget {
List<TranslationDTO> initials = initialValue; List<TranslationDTO> initials = initialValue;
// Préparer les valeurs pour toutes les langues // Préparer les valeurs pour toutes les langues
managerAppContext.selectedConfiguration!.languages!.forEach((lang) { languages.forEach((lang) {
if (initials.any((iv) => iv.language == lang)) { if (initials.any((iv) => iv.language == lang)) {
newValues.add(TranslationDTO.fromJson( newValues.add(TranslationDTO.fromJson(
jsonDecode(jsonEncode(initials.firstWhere((e) => e.language == lang)))!)!); jsonDecode(jsonEncode(initials.firstWhere((e) => e.language == lang)))!)!);

View File

@ -9,6 +9,9 @@ import 'package:manager_app/Components/confirmation_dialog.dart';
import 'package:manager_app/Components/message_notification.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/multi_select_dropdown_language_container.dart';
import 'package:manager_app/Components/card_shape_selector.dart'; import 'package:manager_app/Components/card_shape_selector.dart';
import 'package:manager_app/Components/check_input_container.dart';
import 'package:manager_app/Components/multi_string_input_container.dart';
import 'package:manager_app/Components/string_input_container.dart';
import 'package:manager_app/Components/reorderable_custom_list.dart'; import 'package:manager_app/Components/reorderable_custom_list.dart';
import 'package:manager_app/Components/resource_input_container.dart'; import 'package:manager_app/Components/resource_input_container.dart';
import 'package:manager_app/Components/single_choice_input_container.dart'; import 'package:manager_app/Components/single_choice_input_container.dart';
@ -36,6 +39,7 @@ class AppConfigurationLinkScreen extends StatefulWidget {
class _AppConfigurationLinkScreenState extends State<AppConfigurationLinkScreen> { class _AppConfigurationLinkScreenState extends State<AppConfigurationLinkScreen> {
late ApplicationInstanceDTO _applicationInstanceDTO; late ApplicationInstanceDTO _applicationInstanceDTO;
final Map<String, Timer> _spanDebounceTimers = {}; final Map<String, Timer> _spanDebounceTimers = {};
Timer? _storeUrlDebounceTimer;
// Largeur de viewport simulée pour l'aperçu Web (le site est responsive, // Largeur de viewport simulée pour l'aperçu Web (le site est responsive,
// pas figé l'admin doit pouvoir vérifier les deux). // pas figé l'admin doit pouvoir vérifier les deux).
bool _webPreviewMobileWidth = false; bool _webPreviewMobileWidth = false;
@ -51,6 +55,7 @@ class _AppConfigurationLinkScreenState extends State<AppConfigurationLinkScreen>
for (final timer in _spanDebounceTimers.values) { for (final timer in _spanDebounceTimers.values) {
timer.cancel(); timer.cancel();
} }
_storeUrlDebounceTimer?.cancel();
super.dispose(); super.dispose();
} }
@ -126,6 +131,22 @@ class _AppConfigurationLinkScreenState extends State<AppConfigurationLinkScreen>
ManagerAppContext managerAppContext = appContext.getContext() as ManagerAppContext; ManagerAppContext managerAppContext = appContext.getContext() as ManagerAppContext;
final appUpdatedMsg = widget.appUpdatedLabel ?? AppLocalizations.of(context)!.appUpdatedSuccess; final appUpdatedMsg = widget.appUpdatedLabel ?? AppLocalizations.of(context)!.appUpdatedSuccess;
final isVisitorApp = _applicationInstanceDTO.appType == AppType.Mobile || _applicationInstanceDTO.appType == AppType.Web;
final canEditStoreUrls = _applicationInstanceDTO.appType == AppType.Mobile && managerAppContext.role == UserRole.SuperAdmin;
Future<void> saveApplicationInstance() async {
var applicationInstance = await updateApplicationInstance(appContext, _applicationInstanceDTO);
if (applicationInstance != null && context.mounted) {
showNotification(kSuccess, kWhite, appUpdatedMsg, context, null);
}
}
// Un champ texte notifie à chaque frappe : on n'enregistre qu'une fois la saisie posée.
void saveStoreUrlDebounced() {
_storeUrlDebounceTimer?.cancel();
_storeUrlDebounceTimer = Timer(const Duration(milliseconds: 800), saveApplicationInstance);
}
_generalInfoCard() { _generalInfoCard() {
const elementHeight = 116.0; const elementHeight = 116.0;
@ -334,6 +355,76 @@ class _AppConfigurationLinkScreenState extends State<AppConfigurationLinkScreen>
), ),
), ),
), ),
if (isVisitorApp)
SizedBox(
width: elementWidth,
height: elementHeight,
child: Center(
child: MultiStringInputContainer(
label: AppLocalizations.of(context)!.appNameLabel,
modalLabel: AppLocalizations.of(context)!.appNameModalLabel,
initialValue: _applicationInstanceDTO.appName ?? [],
languages: _applicationInstanceDTO.languages ?? [],
maxLines: 1,
isTitle: true,
isMandatory: false,
onGetResult: (List<TranslationDTO> value) async {
setState(() {
_applicationInstanceDTO.appName = value;
});
await saveApplicationInstance();
},
),
),
),
if (isVisitorApp)
SizedBox(
width: elementWidth,
height: elementHeight,
child: Center(
child: CheckInputContainer(
label: AppLocalizations.of(context)!.qrScanEnabledLabel,
subtitle: AppLocalizations.of(context)!.qrScanEnabledHint,
isChecked: _applicationInstanceDTO.isQRCodeEnabled ?? true,
onChanged: (value) async {
_applicationInstanceDTO.isQRCodeEnabled = value;
await saveApplicationInstance();
},
),
),
),
if (canEditStoreUrls)
SizedBox(
width: elementWidth,
height: elementHeight,
child: Center(
child: StringInputContainer(
label: AppLocalizations.of(context)!.appStoreUrlLabel,
initialValue: _applicationInstanceDTO.appStoreUrl ?? "",
maxLength: 300,
onChanged: (value) {
_applicationInstanceDTO.appStoreUrl = value.trim().isEmpty ? null : value.trim();
saveStoreUrlDebounced();
},
),
),
),
if (canEditStoreUrls)
SizedBox(
width: elementWidth,
height: elementHeight,
child: Center(
child: StringInputContainer(
label: AppLocalizations.of(context)!.playStoreUrlLabel,
initialValue: _applicationInstanceDTO.playStoreUrl ?? "",
maxLength: 300,
onChanged: (value) {
_applicationInstanceDTO.playStoreUrl = value.trim().isEmpty ? null : value.trim();
saveStoreUrlDebounced();
},
),
),
),
// L'activation de l'assistant par canal se règle dans l'onglet Guide IA, // L'activation de l'assistant par canal se règle dans l'onglet Guide IA,
// qui liste tous les canaux au même endroit. // qui liste tous les canaux au même endroit.
], ],

View File

@ -273,7 +273,12 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
Widget _cardQR(AppContext appContext, AppLocalizations l) { Widget _cardQR(AppContext appContext, AppLocalizations l) {
final managerCtx = appContext.getContext() as ManagerAppContext; final managerCtx = appContext.getContext() as ManagerAppContext;
final qrData = "${managerCtx.host!.replaceFirst("api", "web")}/${managerCtx.instanceId}/${managerCtx.selectedConfiguration!.id}/${sectionDTO.id!}"; final instanceDTO = managerCtx.instanceDTO;
// Sans app mobile (offre web seule), la page de téléchargement n'aurait rien à proposer :
// le QR ouvre directement la section dans l'app web.
final qrData = instanceDTO?.isMobile != true && instanceDTO?.isWeb == true && instanceDTO?.webSlug != null
? "$kVisitorWebUrl/${instanceDTO!.webSlug}/${managerCtx.selectedConfiguration!.id}/sections/${sectionDTO.id!}"
: "$kVisitorWebUrl/download/${managerCtx.instanceId}/${managerCtx.selectedConfiguration!.id}/${sectionDTO.id!}";
return Pane( return Pane(
icon: Icons.share_outlined, icon: Icons.share_outlined,

View File

@ -23,6 +23,10 @@ const kSuccess = Color(0xFF8bc34a);
const kApiBaseUrl = String.fromEnvironment('API_BASE_URL', const kApiBaseUrl = String.fromEnvironment('API_BASE_URL',
defaultValue: 'http://localhost:5000'); defaultValue: 'http://localhost:5000');
// App visiteur web (visitapp-web). Les QR des sections pointent vers sa page
// /download, qui présente l'app mobile de l'instance.
const kVisitorWebUrl = 'https://app.myinfomate.be';
// Empreinte du build, injectee elle aussi au build : // Empreinte du build, injectee elle aussi au build :
// --dart-define=GIT_SHA=$(git rev-parse --short HEAD) // --dart-define=GIT_SHA=$(git rev-parse --short HEAD)
// Elle est affichee sous le titre de l'ecran de login et sert a relier un // Elle est affichee sous le titre de l'ecran de login et sert a relier un

View File

@ -637,6 +637,12 @@
"languagesLabel": "Languages:", "languagesLabel": "Languages:",
"featuredEventLabel": "Featured event:", "featuredEventLabel": "Featured event:",
"chooseEvent": "Choose an event", "chooseEvent": "Choose an event",
"appNameLabel": "Application name:",
"appNameModalLabel": "Name shown in the application header",
"qrScanEnabledLabel": "Show the QR code scanner",
"qrScanEnabledHint": "The scan button appears on the home screen and in visits",
"appStoreUrlLabel": "App Store link:",
"playStoreUrlLabel": "Play Store link:",
"previewWebTitle": "Preview — Web", "previewWebTitle": "Preview — Web",
"previewMobileTitle": "Preview — Mobile", "previewMobileTitle": "Preview — Mobile",
"previewDesktop": "Desktop", "previewDesktop": "Desktop",

View File

@ -637,6 +637,12 @@
"languagesLabel": "Langues :", "languagesLabel": "Langues :",
"featuredEventLabel": "Evènement à l'affiche :", "featuredEventLabel": "Evènement à l'affiche :",
"chooseEvent": "Choisir un évènement", "chooseEvent": "Choisir un évènement",
"appNameLabel": "Nom de l'application :",
"appNameModalLabel": "Nom affiché en en-tête de l'application",
"qrScanEnabledLabel": "Afficher le scan de QR code",
"qrScanEnabledHint": "Le bouton de scan apparaît sur l'accueil et dans les visites",
"appStoreUrlLabel": "Lien App Store :",
"playStoreUrlLabel": "Lien Play Store :",
"previewWebTitle": "Aperçu — Web", "previewWebTitle": "Aperçu — Web",
"previewMobileTitle": "Aperçu — Mobile", "previewMobileTitle": "Aperçu — Mobile",
"previewDesktop": "Desktop", "previewDesktop": "Desktop",

View File

@ -2860,6 +2860,42 @@ abstract class AppLocalizations {
/// **'Choisir un évènement'** /// **'Choisir un évènement'**
String get chooseEvent; String get chooseEvent;
/// No description provided for @appNameLabel.
///
/// In fr, this message translates to:
/// **'Nom de l\'application :'**
String get appNameLabel;
/// No description provided for @appNameModalLabel.
///
/// In fr, this message translates to:
/// **'Nom affiché en en-tête de l\'application'**
String get appNameModalLabel;
/// No description provided for @qrScanEnabledLabel.
///
/// In fr, this message translates to:
/// **'Afficher le scan de QR code'**
String get qrScanEnabledLabel;
/// No description provided for @qrScanEnabledHint.
///
/// In fr, this message translates to:
/// **'Le bouton de scan apparaît sur l\'accueil et dans les visites'**
String get qrScanEnabledHint;
/// No description provided for @appStoreUrlLabel.
///
/// In fr, this message translates to:
/// **'Lien App Store :'**
String get appStoreUrlLabel;
/// No description provided for @playStoreUrlLabel.
///
/// In fr, this message translates to:
/// **'Lien Play Store :'**
String get playStoreUrlLabel;
/// No description provided for @previewWebTitle. /// No description provided for @previewWebTitle.
/// ///
/// In fr, this message translates to: /// In fr, this message translates to:

View File

@ -1520,6 +1520,25 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get chooseEvent => 'Choose an event'; String get chooseEvent => 'Choose an event';
@override
String get appNameLabel => 'Application name:';
@override
String get appNameModalLabel => 'Name shown in the application header';
@override
String get qrScanEnabledLabel => 'Show the QR code scanner';
@override
String get qrScanEnabledHint =>
'The scan button appears on the home screen and in visits';
@override
String get appStoreUrlLabel => 'App Store link:';
@override
String get playStoreUrlLabel => 'Play Store link:';
@override @override
String get previewWebTitle => 'Preview — Web'; String get previewWebTitle => 'Preview — Web';

View File

@ -1557,6 +1557,25 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get chooseEvent => 'Choisir un évènement'; String get chooseEvent => 'Choisir un évènement';
@override
String get appNameLabel => 'Nom de l\'application :';
@override
String get appNameModalLabel => 'Nom affiché en en-tête de l\'application';
@override
String get qrScanEnabledLabel => 'Afficher le scan de QR code';
@override
String get qrScanEnabledHint =>
'Le bouton de scan apparaît sur l\'accueil et dans les visites';
@override
String get appStoreUrlLabel => 'Lien App Store :';
@override
String get playStoreUrlLabel => 'Lien Play Store :';
@override @override
String get previewWebTitle => 'Aperçu — Web'; String get previewWebTitle => 'Aperçu — Web';

View File

@ -1536,6 +1536,25 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get chooseEvent => 'Kies een evenement'; String get chooseEvent => 'Kies een evenement';
@override
String get appNameLabel => 'Naam van de applicatie:';
@override
String get appNameModalLabel => 'Naam in de koptekst van de applicatie';
@override
String get qrScanEnabledLabel => 'QR-codescanner tonen';
@override
String get qrScanEnabledHint =>
'De scanknop verschijnt op het startscherm en in de bezoeken';
@override
String get appStoreUrlLabel => 'App Store-link:';
@override
String get playStoreUrlLabel => 'Play Store-link:';
@override @override
String get previewWebTitle => 'Voorbeeld — Web'; String get previewWebTitle => 'Voorbeeld — Web';

View File

@ -637,6 +637,12 @@
"languagesLabel": "Talen:", "languagesLabel": "Talen:",
"featuredEventLabel": "Uitgelicht evenement:", "featuredEventLabel": "Uitgelicht evenement:",
"chooseEvent": "Kies een evenement", "chooseEvent": "Kies een evenement",
"appNameLabel": "Naam van de applicatie:",
"appNameModalLabel": "Naam in de koptekst van de applicatie",
"qrScanEnabledLabel": "QR-codescanner tonen",
"qrScanEnabledHint": "De scanknop verschijnt op het startscherm en in de bezoeken",
"appStoreUrlLabel": "App Store-link:",
"playStoreUrlLabel": "Play Store-link:",
"previewWebTitle": "Voorbeeld — Web", "previewWebTitle": "Voorbeeld — Web",
"previewMobileTitle": "Voorbeeld — Mobiel", "previewMobileTitle": "Voorbeeld — Mobiel",
"previewDesktop": "Desktop", "previewDesktop": "Desktop",

View File

@ -28,6 +28,10 @@ class ApplicationInstanceDTO {
this.sectionEventDTO, this.sectionEventDTO,
this.isAssistant, this.isAssistant,
this.hasStats, this.hasStats,
this.isQRCodeEnabled,
this.appName = const [],
this.appStoreUrl,
this.playStoreUrl,
}); });
String? id; String? id;
@ -66,6 +70,14 @@ class ApplicationInstanceDTO {
bool? hasStats; bool? hasStats;
bool? isQRCodeEnabled;
List<TranslationDTO>? appName;
String? appStoreUrl;
String? playStoreUrl;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@ -84,7 +96,11 @@ class ApplicationInstanceDTO {
other.sectionEventId == sectionEventId && other.sectionEventId == sectionEventId &&
other.sectionEventDTO == sectionEventDTO && other.sectionEventDTO == sectionEventDTO &&
other.isAssistant == isAssistant && other.isAssistant == isAssistant &&
other.hasStats == hasStats; other.hasStats == hasStats &&
other.isQRCodeEnabled == isQRCodeEnabled &&
_deepEquality.equals(other.appName, appName) &&
other.appStoreUrl == appStoreUrl &&
other.playStoreUrl == playStoreUrl;
@override @override
int get hashCode => int get hashCode =>
@ -103,11 +119,15 @@ class ApplicationInstanceDTO {
(sectionEventId == null ? 0 : sectionEventId!.hashCode) + (sectionEventId == null ? 0 : sectionEventId!.hashCode) +
(sectionEventDTO == null ? 0 : sectionEventDTO!.hashCode) + (sectionEventDTO == null ? 0 : sectionEventDTO!.hashCode) +
(isAssistant == null ? 0 : isAssistant!.hashCode) + (isAssistant == null ? 0 : isAssistant!.hashCode) +
(hasStats == null ? 0 : hasStats!.hashCode); (hasStats == null ? 0 : hasStats!.hashCode) +
(isQRCodeEnabled == null ? 0 : isQRCodeEnabled!.hashCode) +
(appName == null ? 0 : appName!.hashCode) +
(appStoreUrl == null ? 0 : appStoreUrl!.hashCode) +
(playStoreUrl == null ? 0 : playStoreUrl!.hashCode);
@override @override
String toString() => String toString() =>
'ApplicationInstanceDTO[id=$id, instanceId=$instanceId, appType=$appType, configurations=$configurations, mainImageId=$mainImageId, mainImageUrl=$mainImageUrl, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, languages=$languages, sectionEventId=$sectionEventId, sectionEventDTO=$sectionEventDTO, isAssistant=$isAssistant, hasStats=$hasStats]'; 'ApplicationInstanceDTO[id=$id, instanceId=$instanceId, appType=$appType, configurations=$configurations, mainImageId=$mainImageId, mainImageUrl=$mainImageUrl, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, languages=$languages, sectionEventId=$sectionEventId, sectionEventDTO=$sectionEventDTO, isAssistant=$isAssistant, hasStats=$hasStats, isQRCodeEnabled=$isQRCodeEnabled, appName=$appName, appStoreUrl=$appStoreUrl, playStoreUrl=$playStoreUrl]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -186,6 +206,24 @@ class ApplicationInstanceDTO {
} else { } else {
json[r'hasStats'] = null; json[r'hasStats'] = null;
} }
if (this.isQRCodeEnabled != null) {
json[r'isQRCodeEnabled'] = this.isQRCodeEnabled;
}
if (this.appName != null) {
json[r'appName'] = this.appName;
} else {
json[r'appName'] = null;
}
if (this.appStoreUrl != null) {
json[r'appStoreUrl'] = this.appStoreUrl;
} else {
json[r'appStoreUrl'] = null;
}
if (this.playStoreUrl != null) {
json[r'playStoreUrl'] = this.playStoreUrl;
} else {
json[r'playStoreUrl'] = null;
}
return json; return json;
} }
@ -230,6 +268,10 @@ class ApplicationInstanceDTO {
sectionEventDTO: SectionEventDTO.fromJson(json[r'sectionEventDTO']), sectionEventDTO: SectionEventDTO.fromJson(json[r'sectionEventDTO']),
isAssistant: mapValueOfType<bool>(json, r'isAssistant'), isAssistant: mapValueOfType<bool>(json, r'isAssistant'),
hasStats: mapValueOfType<bool>(json, r'hasStats'), hasStats: mapValueOfType<bool>(json, r'hasStats'),
isQRCodeEnabled: mapValueOfType<bool>(json, r'isQRCodeEnabled'),
appName: TranslationDTO.listFromJson(json[r'appName']),
appStoreUrl: mapValueOfType<String>(json, r'appStoreUrl'),
playStoreUrl: mapValueOfType<String>(json, r'playStoreUrl'),
); );
} }
return null; return null;