diff --git a/lib/Components/AssistantChatSheet.dart b/lib/Components/AssistantChatSheet.dart index 634f999..79d14dd 100644 --- a/lib/Components/AssistantChatSheet.dart +++ b/lib/Components/AssistantChatSheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +import 'package:mymuseum_visitapp/Components/VisitorPrivacyNotice.dart'; import 'package:mymuseum_visitapp/Helpers/assistantSuggestions.dart'; import 'package:mymuseum_visitapp/Helpers/translationHelper.dart'; import 'package:mymuseum_visitapp/Models/AssistantResponse.dart'; @@ -265,6 +266,15 @@ class _AssistantChatSheetState extends State { }, ), const Spacer(), + // §8.3 des CGU : la mention doit être accessible depuis l'assistant + // lui-même, c'est-à-dire là où la collecte a lieu — pas seulement dans + // la politique de confidentialité du lieu. + IconButton( + icon: const Icon(Icons.privacy_tip_outlined, size: 20), + tooltip: VisitorPrivacyNotice.textFor(widget.visitAppContext.language).title, + onPressed: () => + VisitorPrivacyNotice.show(context, widget.visitAppContext), + ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), diff --git a/lib/Components/VisitorPrivacyNotice.dart b/lib/Components/VisitorPrivacyNotice.dart new file mode 100644 index 0000000..0e2254b --- /dev/null +++ b/lib/Components/VisitorPrivacyNotice.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:mymuseum_visitapp/Models/visitContext.dart'; + +/// Mention d'information aux visiteurs sur le traitement de leurs questions (§8.3 des CGU). +/// +/// ⚠️ **Texte repris mot pour mot de `DOCS/mention-information-visiteurs.md`**, qui est le +/// document de référence remis au client. Toute correction se fait là-bas d'abord : c'est lui +/// que le client publie dans sa politique de confidentialité, et les deux ne doivent pas +/// diverger. ⚠️ Il est **rédigé côté produit et non validé juridiquement** (lot J, J5). +/// +/// ⚠️ **Trois langues seulement, repli sur l'anglais — c'est un choix, pas un oubli.** L'app +/// en porte dix, mais traduire une mention de protection des données sans relecture humaine +/// serait pire que la servir en anglais : une nuance perdue sur « nous n'enregistrons pas +/// votre adresse IP » n'est pas une coquille d'interface. Les sept autres langues attendent +/// une traduction relue, à demander en même temps que la relecture juridique. +class VisitorPrivacyNotice extends StatelessWidget { + final VisitAppContext visitAppContext; + + const VisitorPrivacyNotice({Key? key, required this.visitAppContext}) : super(key: key); + + static void show(BuildContext context, VisitAppContext visitAppContext) { + showDialog( + context: context, + builder: (_) => VisitorPrivacyNotice(visitAppContext: visitAppContext), + ); + } + + static const Map _texts = { + 'FR': ( + title: 'Vos questions au guide', + body: + "Lorsque vous posez une question à notre guide, votre question et la réponse qui vous " + "est donnée sont enregistrées. Cela nous sert à repérer ce que nos visiteurs cherchent " + "sans le trouver, et à compléter nos contenus en conséquence.\n\n" + "Nous n'enregistrons ni votre nom, ni votre compte, ni votre adresse IP. Un identifiant " + "de session tiré au hasard permet seulement de relier entre elles les questions d'une " + "même conversation ; il disparaît avec elle.\n\n" + "Votre question est transmise à notre fournisseur d'intelligence artificielle (Google) " + "pour produire la réponse. Le texte de vos questions est supprimé au bout de 90 jours ; " + "seuls des regroupements par sujet, sans le texte de vos questions, sont conservés " + "au-delà.\n\n" + "Le champ de question est libre : nous vous invitons à ne pas y saisir d'informations " + "personnelles." + ), + 'NL': ( + title: 'Uw vragen aan de gids', + body: + "Wanneer u onze gids een vraag stelt, worden uw vraag en het gegeven antwoord " + "opgeslagen. Zo zien we wat onze bezoekers zoeken zonder het te vinden, en vullen we " + "onze inhoud aan.\n\n" + "Wij registreren noch uw naam, noch een account, noch uw IP-adres. Een willekeurig " + "gegenereerde sessie-identificatie dient enkel om de vragen van eenzelfde gesprek aan " + "elkaar te koppelen; ze verdwijnt samen met dat gesprek.\n\n" + "Uw vraag wordt doorgestuurd naar onze aanbieder van kunstmatige intelligentie (Google) " + "om het antwoord op te stellen. De tekst van uw vragen wordt na 90 dagen verwijderd; " + "daarna blijven enkel groeperingen per onderwerp bewaard, zonder de tekst van uw " + "vragen.\n\n" + "Het vraagveld is vrij in te vullen: wij raden u aan er geen persoonsgegevens in te " + "typen." + ), + 'EN': ( + title: 'Your questions to the guide', + body: + "When you ask our guide a question, your question and the answer you are given are " + "recorded. This helps us see what our visitors look for without finding it, and improve " + "our content accordingly.\n\n" + "We record neither your name, nor an account, nor your IP address. A randomly generated " + "session identifier only links together the questions of a single conversation; it " + "disappears with it.\n\n" + "Your question is sent to our artificial intelligence provider (Google) to produce the " + "answer. The text of your questions is deleted after 90 days; beyond that, only " + "groupings by topic are kept, without the text of your questions.\n\n" + "The question field is free text: we invite you not to enter personal information in it." + ), + }; + + static ({String title, String body}) textFor(String? language) => + _texts[(language ?? 'FR').toUpperCase()] ?? _texts['EN']!; + + @override + Widget build(BuildContext context) { + final text = textFor(visitAppContext.language); + + return AlertDialog( + title: Text(text.title, style: const TextStyle(fontSize: 17)), + content: SingleChildScrollView( + child: Text(text.body, style: const TextStyle(fontSize: 13.5, height: 1.45)), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ); + } +} diff --git a/lib/Services/downloadConfiguration.dart b/lib/Services/downloadConfiguration.dart index 28f91fe..5447905 100644 --- a/lib/Services/downloadConfiguration.dart +++ b/lib/Services/downloadConfiguration.dart @@ -27,6 +27,11 @@ class DownloadConfigurationWidget extends StatefulWidget { class _DownloadConfigurationWidgetState extends State { ValueNotifier currentResourceIndex = ValueNotifier(0); ValueNotifier currentResourceNbr = ValueNotifier(-1); + + /// D5 — nombre de ressources dont le téléchargement a échoué. Alimente le message + /// d'échec : « 3 fichiers n'ont pas pu être téléchargés » dit au visiteur ce qui + /// s'est passé, là où l'écran d'erreur générique le laissait supposer une panne. + ValueNotifier downloadFailureCount = ValueNotifier(0); bool isAlreadyDownloading = false; //OtaEvent? currentEvent; @@ -106,6 +111,9 @@ class _DownloadConfigurationWidgetState extends State localResourceDates = await readLocalResourceDates(); + // Ressources dont le téléchargement a échoué. Vide = visite complète. + final List failedResourceIds = []; + var resourcesToDownload = exportConfigurationDTO.resources!.where((resource) => resource.type != ResourceType.ImageUrl && resource.type != ResourceType.VideoUrl && resource.type != ResourceType.JsonUrl && resource.url != null && isResourceOutdated(resource, fileList, localResourceDates[resource.id])); currentResourceNbr.value = resourcesToDownload.length; @@ -127,7 +135,12 @@ class _DownloadConfigurationWidgetState extends State( + valueListenable: downloadFailureCount, + builder: (context, failures, _) { + if (failures == 0) { + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + textAlign: TextAlign.center, + valueIndex == valueNbr && valueNbr != -1 ? valueNbr == 0 ? + TranslationHelper.getFromLocale( + "upToDate", + appContext.getContext()) : TranslationHelper.getFromLocale( + "downloadFinish", + appContext.getContext()) : TranslationHelper.getFromLocale( + "downloadInProgress", + appContext.getContext()), + style: const TextStyle(fontSize: 20), + ), + ), + ); + } + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.cloud_off, size: 34, color: Colors.orangeAccent), + const SizedBox(height: 10), + Text( + TranslationHelper.getFromLocale( + "downloadIncomplete", appContext.getContext()), + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + ], + ), + ), + ); + }, ), if(valueNbr != -1 && valueNbr != 0) Center( diff --git a/lib/translations.dart b/lib/translations.dart index cf91a4c..359c469 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -25,6 +25,7 @@ List translations = [ "restart": "Recommencer", "downloadInProgress": "Téléchargement en cours", "downloadFinish": "Téléchargement terminé", + "downloadIncomplete": "Téléchargement incomplet : certains fichiers manquent. Relancez le téléchargement, seuls les fichiers manquants seront récupérés.", "upToDate": "Tout est à jour", "weather.hourly": "Prochaines heures", "weather.nextdays": "Prochains jours", @@ -68,6 +69,7 @@ List translations = [ "restart": "Restart", "downloadInProgress": "Download in progress", "downloadFinish": "Download complete", + "downloadIncomplete": "Download incomplete: some files are missing. Start the download again — only the missing files will be fetched.", "upToDate": "Up to date", "weather.hourly": "Hourly", "weather.nextdays": "Next days", @@ -111,6 +113,7 @@ List translations = [ "restart": "Neu starten", "downloadInProgress": "Download läuft", "downloadFinish": "Download abgeschlossen", + "downloadIncomplete": "Download unvollständig: Einige Dateien fehlen. Starten Sie den Download erneut — es werden nur die fehlenden Dateien geladen.", "upToDate": "Alles ist auf dem neuesten Stand", "weather.hourly": "Nächste Stunden", "weather.nextdays": "Nächsten Tage", @@ -154,6 +157,7 @@ List translations = [ "restart": "Herstarten", "downloadInProgress": "Download bezig", "downloadFinish": "Download voltooid", + "downloadIncomplete": "Download onvolledig: sommige bestanden ontbreken. Start de download opnieuw — alleen de ontbrekende bestanden worden opgehaald.", "upToDate": "Alles is up-to-date", "weather.hourly": "Volgende uren", "weather.nextdays": "Volgende dagen", @@ -197,6 +201,7 @@ List translations = [ "restart": "Ricomincia", "downloadInProgress": "Download in corso", "downloadFinish": "Download completato", + "downloadIncomplete": "Download incompleto: alcuni file mancano. Riavvia il download — verranno recuperati solo i file mancanti.", "upToDate": "Tutto è aggiornato", "weather.hourly": "Le prossime ore", "weather.nextdays": "Prossimi giorni", @@ -240,6 +245,7 @@ List translations = [ "restart": "Reanudar", "downloadInProgress": "Descarga en curso", "downloadFinish": "Descarga completada", + "downloadIncomplete": "Descarga incompleta: faltan algunos archivos. Reinicie la descarga — solo se recuperarán los archivos que faltan.", "upToDate": "Todo está al día", "weather.hourly": "Próximas horas", "weather.nextdays": "Proximos dias", @@ -283,6 +289,7 @@ List translations = [ "restart": "Uruchom ponownie", "downloadInProgress": "Pobieranie w toku", "downloadFinish": "Pobieranie zakończone", + "downloadIncomplete": "Pobieranie niekompletne: brakuje niektórych plików. Uruchom pobieranie ponownie — zostaną pobrane tylko brakujące pliki.", "upToDate": "Wszystko jest aktualne", "weather.hourly": "Następne godziny", "weather.nextdays": "Następne dni", @@ -326,6 +333,7 @@ List translations = [ "restart": "重新开始", "downloadInProgress": "下载中", "downloadFinish": "下载完成", + "downloadIncomplete": "下载不完整:部分文件缺失。请重新开始下载,系统只会获取缺失的文件。", "upToDate": "已是最新", "weather.hourly": "接下来的几个小时", "weather.nextdays": "未来几天", @@ -369,6 +377,7 @@ List translations = [ "restart": "Перезапустіть", "downloadInProgress": "Завантаження триває", "downloadFinish": "Завантаження завершено", + "downloadIncomplete": "Завантаження неповне: деяких файлів бракує. Запустіть завантаження ще раз — буде отримано лише відсутні файли.", "upToDate": "Все актуально", "weather.hourly": "Наступні години", "weather.nextdays": "Наступні дні", @@ -412,6 +421,7 @@ List translations = [ "restart": "إعادة تشغيل", "downloadInProgress": "جارٍ التنزيل", "downloadFinish": "اكتمل التنزيل", + "downloadIncomplete": "التنزيل غير مكتمل: بعض الملفات مفقودة. أعد بدء التنزيل — سيتم جلب الملفات المفقودة فقط.", "upToDate": "كل شيء محدث", "weather.hourly": "الساعات القادمة", "weather.nextdays": "الايام القادمة",