Misc translate html + use mapbox default (offline mode) + fix offline mode + small fix home 3 layout
This commit is contained in:
parent
2fd2132b31
commit
84ccaffcb5
@ -124,6 +124,7 @@ android {
|
||||
signingConfig signingConfigs.release
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
debuggable false
|
||||
}
|
||||
}
|
||||
|
||||
8
android/app/proguard-rules.pro
vendored
Normal file
8
android/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Le SDK Meta Wearables (meta_wearables_dat) référence des annotations et utilitaires
|
||||
# Facebook internes qui ne sont pas publiés dans ses artefacts. R8 les signale comme
|
||||
# classes manquantes et échoue le minify du build release.
|
||||
-dontwarn com.facebook.annotations.DoNotOptimize
|
||||
-dontwarn com.facebook.common.preconditions.Preconditions
|
||||
-dontwarn com.facebook.infer.annotation.Nullsafe
|
||||
-dontwarn com.facebook.infer.annotation.NullsafeStrict
|
||||
-dontwarn com.facebook.secure.sanitizer.intf.DataSanitizer
|
||||
@ -12,7 +12,6 @@ import 'package:mymuseum_visitapp/Services/meta_glasses_service.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:speech_to_text/speech_to_text.dart';
|
||||
|
||||
String _stripHtml(String html) => html.replaceAll(RegExp(r'<[^>]*>'), '').trim();
|
||||
|
||||
class AssistantChatSheet extends StatefulWidget {
|
||||
final VisitAppContext visitAppContext;
|
||||
@ -172,7 +171,7 @@ class _AssistantChatSheetState extends State<AssistantChatSheet> {
|
||||
String get _venueName {
|
||||
final configuration = widget.visitAppContext.configuration;
|
||||
if (configuration == null) return '';
|
||||
final title = _stripHtml(
|
||||
final title = TranslationHelper.stripHtml(
|
||||
TranslationHelper.get(configuration.title, widget.visitAppContext));
|
||||
return title.isNotEmpty ? title : (configuration.label ?? '');
|
||||
}
|
||||
@ -653,7 +652,7 @@ class _AssistantMessage extends StatelessWidget {
|
||||
Navigator.of(context).pop();
|
||||
onNavigate!(
|
||||
response.navigation!.sectionId,
|
||||
_stripHtml(response.navigation!.sectionTitle),
|
||||
TranslationHelper.stripHtml(response.navigation!.sectionTitle),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
@ -700,7 +699,7 @@ class _AssistantMessage extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_stripHtml(response.navigation!.sectionTitle),
|
||||
TranslationHelper.stripHtml(response.navigation!.sectionTitle),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
|
||||
@ -8,8 +8,12 @@ import 'package:provider/provider.dart';
|
||||
/// Icône circulaire positionnée en haut à droite de l'écran d'accueil.
|
||||
///
|
||||
/// Visible si :
|
||||
/// - Les lunettes sont connectées (quel que soit le mode)
|
||||
/// - OU un mode vocal est actif (voiceOnly ou glasses)
|
||||
/// - La fonctionnalité est activée sur l'instance
|
||||
/// - OU un mode vocal est encore actif — porte de sortie pour le couper si
|
||||
/// l'instance a été désactivée entre-temps.
|
||||
///
|
||||
/// Des lunettes appairées ne suffisent pas : sans la fonctionnalité sur
|
||||
/// l'instance, il n'y a rien à en faire.
|
||||
///
|
||||
/// Tap → ouvre VoiceModeSheet.
|
||||
class GlassesStatusWidget extends StatelessWidget {
|
||||
@ -29,7 +33,7 @@ class GlassesStatusWidget extends StatelessWidget {
|
||||
final modeActive = controller.mode != VoiceMode.none;
|
||||
final featureAvailable = controller.isFeatureAvailable(visitAppContext);
|
||||
|
||||
if (!featureAvailable && !isConnected && !modeActive) return const SizedBox.shrink();
|
||||
if (!featureAvailable && !modeActive) return const SizedBox.shrink();
|
||||
|
||||
final Color iconColor = switch ((isConnected, controller.mode)) {
|
||||
(true, VoiceMode.glasses) => Colors.greenAccent,
|
||||
|
||||
37
lib/Components/HtmlText.dart
Normal file
37
lib/Components/HtmlText.dart
Normal file
@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
|
||||
/// Rendu d'un champ traduit. Tous les champs saisis dans le manager passent par
|
||||
/// l'éditeur riche : leur valeur est du HTML, titres compris.
|
||||
///
|
||||
/// La marge par défaut des `<p>` est neutralisée pour que le bloc s'aligne comme
|
||||
/// un `Text` à la place duquel il est posé.
|
||||
class HtmlText extends StatelessWidget {
|
||||
const HtmlText(this.html, {super.key, this.style, this.textAlign});
|
||||
|
||||
final String html;
|
||||
|
||||
/// Par défaut, le style ambiant — celui que `ListTile` applique à son titre,
|
||||
/// par exemple.
|
||||
final TextStyle? style;
|
||||
final TextAlign? textAlign;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HtmlWidget(
|
||||
html,
|
||||
textStyle: style ?? DefaultTextStyle.of(context).style,
|
||||
customStylesBuilder: (_) => {
|
||||
'margin': '0',
|
||||
if (textAlign != null) 'text-align': _cssAlign(textAlign!),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static String _cssAlign(TextAlign align) => switch (align) {
|
||||
TextAlign.center => 'center',
|
||||
TextAlign.right || TextAlign.end => 'right',
|
||||
TextAlign.justify => 'justify',
|
||||
_ => 'left',
|
||||
};
|
||||
}
|
||||
@ -63,12 +63,12 @@ class VoiceModeSheet extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Section lunettes — toujours visible
|
||||
_GlassesConnectionTile(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Modes — uniquement si isAssistant
|
||||
if (controller.isFeatureAvailable(visitAppContext)) ...[
|
||||
// Lunettes et modes — uniquement si la fonctionnalité est activée
|
||||
// sur l'instance, ou si un mode est resté actif (pour le couper).
|
||||
if (controller.isFeatureAvailable(visitAppContext) ||
|
||||
controller.mode != VoiceMode.none) ...[
|
||||
_GlassesConnectionTile(),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Mode',
|
||||
style: TextStyle(color: Colors.white54, fontSize: 12, letterSpacing: 1),
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
|
||||
/// Questions proposées au visiteur quand il ouvre l'assistant.
|
||||
@ -69,8 +70,6 @@ class AssistantSuggestions {
|
||||
return name == null ? template : template.replaceAll('{name}', name);
|
||||
}
|
||||
|
||||
static String _stripHtml(String value) =>
|
||||
value.replaceAll(RegExp(r'<[^>]*>'), '').trim();
|
||||
|
||||
static String _titleOf(Map<String, dynamic> section, String lang) {
|
||||
final titles = section['title'];
|
||||
@ -80,7 +79,7 @@ class AssistantSuggestions {
|
||||
orElse: () => titles.first,
|
||||
);
|
||||
if (match is! Map) return '';
|
||||
return _stripHtml((match['value'] as String?) ?? '');
|
||||
return TranslationHelper.stripHtml((match['value'] as String?) ?? '');
|
||||
}
|
||||
|
||||
static List<String> build(VisitAppContext context, {String? currentSectionId}) {
|
||||
|
||||
@ -20,7 +20,13 @@ class ModelsHelper {
|
||||
};
|
||||
}
|
||||
|
||||
static Map<String, dynamic> sectionToMap(SectionDTO section) {
|
||||
/// [rawSectionJson] est la réponse brute de `sectionGetDetail`, encodée en JSON.
|
||||
/// `SectionDTO` ne porte que l'en-tête commun : depuis la v3, le payload typé
|
||||
/// (contenus d'article, questions de quiz, points de carte) vit dans les sous-DTO
|
||||
/// et n'existe que dans cette réponse brute. Sans elle, une section relue hors
|
||||
/// ligne n'a rien à afficher — et la colonne `data`, déclarée NOT NULL, faisait
|
||||
/// échouer l'insertion, donc la table restait vide.
|
||||
static Map<String, dynamic> sectionToMap(SectionDTO section, String rawSectionJson) {
|
||||
return {
|
||||
'id': section.id,
|
||||
'instanceId': section.instanceId,
|
||||
@ -33,7 +39,7 @@ class ModelsHelper {
|
||||
'isSubSection': section.isSubSection,
|
||||
'parentId': section.parentId,
|
||||
'type': section.type!.value,
|
||||
//'data': section.data, // TODO section data
|
||||
'data': rawSectionJson,
|
||||
'dateCreation': section.dateCreation!.toUtc().toIso8601String(),
|
||||
'orderOfElement': section.order,
|
||||
};
|
||||
|
||||
@ -19,6 +19,21 @@ class TranslationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tous les champs traduits sont saisis dans l'éditeur riche du manager :
|
||||
/// leur valeur est du HTML. À utiliser quand le rendu ne peut pas être un
|
||||
/// HtmlWidget — texte interpolé dans une phrase, comparaison, recherche.
|
||||
static String stripHtml(String html) => html
|
||||
.replaceAll(RegExp(r'<[^>]*>', multiLine: true), ' ')
|
||||
.replaceAll(' ', ' ')
|
||||
.replaceAll(RegExp(r'\s+'), ' ')
|
||||
.trim();
|
||||
|
||||
static String getPlain(List<TranslationDTO>? translationDTO, VisitAppContext visitAppContext) =>
|
||||
stripHtml(get(translationDTO, visitAppContext));
|
||||
|
||||
static String getWithResourcePlain(List<TranslationAndResourceDTO>? translationAndResourceDTO, VisitAppContext visitAppContext) =>
|
||||
stripHtml(getWithResource(translationAndResourceDTO, visitAppContext));
|
||||
|
||||
static String getFromLocale(String valueToGet, VisitAppContext visitAppContext) {
|
||||
try {
|
||||
return translations.where((element) => element.language == visitAppContext.language).first.data![valueToGet]!;
|
||||
|
||||
@ -284,7 +284,7 @@ class _BodyState extends State<Body> {
|
||||
|
||||
if (searchValue != null && searchValue!.isNotEmpty) {
|
||||
result = result.where((s) =>
|
||||
removeDiacritics(TranslationHelper.get(s.title, visitAppContext).toLowerCase())
|
||||
removeDiacritics(TranslationHelper.getPlain(s.title, visitAppContext).toLowerCase())
|
||||
.contains(removeDiacritics(searchValue!.toLowerCase()))
|
||||
).toList();
|
||||
} else if (searchNumberValue != null) {
|
||||
|
||||
@ -207,7 +207,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
);
|
||||
|
||||
if (matches.isNotEmpty) {
|
||||
matches = matches.where((bs) => bs!.configurationId == visitAppContext.configuration!.id!);
|
||||
matches = matches.where((bs) => bs!.configurationId == widget.configuration.id);
|
||||
}
|
||||
|
||||
if (matches.isNotEmpty && !modeDebugBeacon) {
|
||||
@ -374,7 +374,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
),
|
||||
),
|
||||
),
|
||||
visitAppContext.beaconSections != null && visitAppContext.beaconSections!.where((bs) => bs!.configurationId == visitAppContext.configuration!.id).isNotEmpty ? Align(
|
||||
visitAppContext.beaconSections != null && visitAppContext.beaconSections!.where((bs) => bs!.configurationId == widget.configuration.id).isNotEmpty ? Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 90, bottom: 1),
|
||||
|
||||
@ -140,36 +140,40 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Title at bottom-left — hauteur bornée + clip : empêche le débordement
|
||||
// même si le moteur HTML natif ignore -webkit-line-clamp (contenu Quill).
|
||||
// Titre + chevron sur une même ligne : le chevron reste centré
|
||||
// verticalement sur le bloc de titre, quel que soit son nombre de lignes.
|
||||
Positioned(
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
right: 28,
|
||||
child: ClipRect(
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: HtmlWidget(
|
||||
cleanedTitle,
|
||||
textStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
customStylesBuilder: (_) => {
|
||||
'font-family': 'Roboto',
|
||||
'-webkit-line-clamp': '2',
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Chevron
|
||||
const Positioned(
|
||||
bottom: 10,
|
||||
right: 8,
|
||||
child: Icon(Icons.chevron_right, size: 18, color: Colors.white),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRect(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 40),
|
||||
child: HtmlWidget(
|
||||
cleanedTitle,
|
||||
textStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
customStylesBuilder: (_) => {
|
||||
'font-family': 'Roboto',
|
||||
'margin': '0',
|
||||
'-webkit-line-clamp': '2',
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.chevron_right, size: 18, color: Colors.white),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -211,23 +215,33 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
const SizedBox(height: 20),
|
||||
// Entrée assistant vocal (si feature disponible)
|
||||
if (visitAppContext.applicationInstanceDTO?.isAssistant == true) ...[
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.mic, color: kMainColor1),
|
||||
title: const Text('Assistant vocal', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
subtitle: const Text('Lunettes ou micro téléphone'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
VoiceModeSheet.show(ctx, visitAppContext: visitAppContext);
|
||||
},
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.mic, color: kMainColor1),
|
||||
title: Text(
|
||||
TranslationHelper.getFromLocale('settings.voiceAssistant', ctx2),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(TranslationHelper.getFromLocale('settings.voiceAssistantSubtitle', ctx2)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
VoiceModeSheet.show(ctx, visitAppContext: visitAppContext);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
const Text('Langue', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
|
||||
Text(
|
||||
TranslationHelper.getFromLocale('settings.language', ctx2),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: configLanguages.map((lang) {
|
||||
final isSelected = ctx2.language == lang;
|
||||
return GestureDetector(
|
||||
@ -245,7 +259,7 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
shape: BoxShape.circle,
|
||||
border: isSelected ? Border.all(color: kMainColor, width: 2.5) : null,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.contain,
|
||||
fit: BoxFit.cover,
|
||||
image: AssetImage('assets/images/old/${lang.toLowerCase()}.png'),
|
||||
),
|
||||
boxShadow: const [BoxShadow(color: kSecondGrey, spreadRadius: 0.5, blurRadius: 5, offset: Offset(0, 1.5))],
|
||||
@ -261,7 +275,10 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Notifications push', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
|
||||
Text(
|
||||
TranslationHelper.getFromLocale('settings.notifications', ctx2),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
|
||||
),
|
||||
Switch(
|
||||
value: ctx2.notificationsEnabled,
|
||||
activeThumbColor: kMainColor,
|
||||
@ -398,16 +415,25 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (titleEntry != null)
|
||||
Text(
|
||||
titleEntry.value ?? '',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.2,
|
||||
ClipRect(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 52),
|
||||
child: HtmlWidget(
|
||||
(titleEntry.value ?? '')
|
||||
.replaceAll('\n', ' ')
|
||||
.replaceAll('<br>', ' '),
|
||||
textStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.2,
|
||||
),
|
||||
customStylesBuilder: (_) => {
|
||||
'margin': '0',
|
||||
'-webkit-line-clamp': '2',
|
||||
},
|
||||
),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
@ -443,7 +469,7 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'Découvrir',
|
||||
TranslationHelper.getFromLocale('event.discover', visitAppContext),
|
||||
style: TextStyle(
|
||||
color: kMainColor,
|
||||
fontSize: 12,
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/HtmlText.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Event/event_map_full_page.dart';
|
||||
@ -108,6 +109,7 @@ class _EventPageState extends State<EventPage> {
|
||||
|
||||
Widget _buildHero(BuildContext context) {
|
||||
final title = _safeTranslate(widget.section.title);
|
||||
final titlePlain = _safeTranslatePlain(widget.section.title);
|
||||
final dateRange = _formatDateRange();
|
||||
final expandedHeight = MediaQuery.of(context).size.height * 0.52;
|
||||
|
||||
@ -117,7 +119,7 @@ class _EventPageState extends State<EventPage> {
|
||||
pinned: true,
|
||||
backgroundColor: kMainColor,
|
||||
title: Text(
|
||||
title,
|
||||
titlePlain,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@ -163,7 +165,7 @@ class _EventPageState extends State<EventPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
HtmlText(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
@ -171,8 +173,6 @@ class _EventPageState extends State<EventPage> {
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.2,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (dateRange.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
@ -292,7 +292,7 @@ class _EventPageState extends State<EventPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
HtmlText(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isActive ? Colors.white : Colors.white.withOpacity(0.9),
|
||||
@ -370,7 +370,7 @@ class _EventPageState extends State<EventPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
HtmlText(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
@ -380,7 +380,7 @@ class _EventPageState extends State<EventPage> {
|
||||
),
|
||||
if (desc.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
HtmlText(
|
||||
desc,
|
||||
style: TextStyle(color: Colors.grey[400], fontSize: 14, height: 1.6),
|
||||
),
|
||||
@ -399,7 +399,7 @@ class _EventPageState extends State<EventPage> {
|
||||
Icon(Icons.place, color: kMainColor, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
child: HtmlText(
|
||||
label,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||
),
|
||||
@ -614,7 +614,7 @@ class _EventPageState extends State<EventPage> {
|
||||
}
|
||||
|
||||
Widget _buildParcoursChip(BuildContext context, GuidedPathDTO path) {
|
||||
final title = _safeTranslate(path.title);
|
||||
final title = _safeTranslatePlain(path.title);
|
||||
final stepCount = path.steps?.length ?? 0;
|
||||
|
||||
return GestureDetector(
|
||||
@ -700,7 +700,7 @@ class _EventPageState extends State<EventPage> {
|
||||
const SizedBox(height: 8),
|
||||
..._paths.map((path) {
|
||||
final title = _safeTranslate(path.title);
|
||||
final desc = _safeTranslate(path.description);
|
||||
final desc = _safeTranslatePlain(path.description);
|
||||
final stepCount = path.steps?.length ?? 0;
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@ -713,7 +713,7 @@ class _EventPageState extends State<EventPage> {
|
||||
),
|
||||
child: Icon(Icons.map_outlined, color: kMainColor, size: 20),
|
||||
),
|
||||
title: Text(
|
||||
title: HtmlText(
|
||||
title,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500),
|
||||
),
|
||||
@ -766,4 +766,9 @@ class _EventPageState extends State<EventPage> {
|
||||
return list.first.value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
/// Version sans balises, pour les emplacements qui exigent un ellipsis strict.
|
||||
String _safeTranslatePlain(List<TranslationDTO>? list) =>
|
||||
TranslationHelper.stripHtml(_safeTranslate(list));
|
||||
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
|
||||
import 'package:mymuseum_visitapp/Components/SliderImages.dart';
|
||||
import 'package:mymuseum_visitapp/Components/HtmlText.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
@ -124,6 +125,9 @@ class _GuidedPathContentProgressionPageState
|
||||
String _translate(List<TranslationDTO>? list) =>
|
||||
TranslationHelper.get(list, widget.visitAppContext);
|
||||
|
||||
String _translatePlain(List<TranslationDTO>? list) =>
|
||||
TranslationHelper.stripHtml(_translate(list));
|
||||
|
||||
bool get _canAdvance => _challenge?.canAdvance ?? true;
|
||||
|
||||
void _advance() {
|
||||
@ -213,13 +217,13 @@ class _GuidedPathContentProgressionPageState
|
||||
}
|
||||
|
||||
void _showEnd() {
|
||||
final outro = _stripHtml(
|
||||
final outro = TranslationHelper.stripHtml(
|
||||
TranslationHelper.getWithResource(widget.path.gameMessageFin, widget.visitAppContext));
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (_) => GuidedPathEndView(
|
||||
isGame: _isGame,
|
||||
primaryColor: kMainColor,
|
||||
pathTitle: _translate(widget.path.title),
|
||||
pathTitle: _translatePlain(widget.path.title),
|
||||
stepsCount: _steps.length,
|
||||
estimatedDurationMinutes: widget.path.estimatedDurationMinutes,
|
||||
gameOutro: outro,
|
||||
@ -231,12 +235,6 @@ class _GuidedPathContentProgressionPageState
|
||||
));
|
||||
}
|
||||
|
||||
String _stripHtml(String html) => html
|
||||
.replaceAll(RegExp(r'<[^>]*>'), ' ')
|
||||
.replaceAll(' ', ' ')
|
||||
.replaceAll(RegExp(r'\s+'), ' ')
|
||||
.trim();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final step = _currentStep;
|
||||
@ -389,7 +387,7 @@ class _GuidedPathContentProgressionPageState
|
||||
height: 200, width: double.infinity, fit: BoxFit.cover),
|
||||
),
|
||||
if (hasContents || step.imageUrl != null) const SizedBox(height: 16),
|
||||
Text(title,
|
||||
HtmlText(title,
|
||||
style: TextStyle(
|
||||
color: _ink, fontSize: 22, fontWeight: FontWeight.w600, height: 1.15)),
|
||||
const SizedBox(height: 12),
|
||||
@ -399,7 +397,7 @@ class _GuidedPathContentProgressionPageState
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: _isGame ? _gold.withOpacity(0.12) : _accent.withOpacity(0.1),
|
||||
),
|
||||
child: Text(_translate(widget.path.title).toUpperCase(),
|
||||
child: Text(_translatePlain(widget.path.title).toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: _accent,
|
||||
fontSize: 11.5,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/HtmlText.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_map_progression_page.dart';
|
||||
@ -67,9 +68,9 @@ class GuidedPathListSheet extends StatelessWidget {
|
||||
),
|
||||
child: const Icon(Icons.map_outlined, color: Colors.blue, size: 20),
|
||||
),
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
title: HtmlText(title, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: desc.isNotEmpty
|
||||
? Text(desc, maxLines: 1, overflow: TextOverflow.ellipsis)
|
||||
? Text(TranslationHelper.stripHtml(desc), maxLines: 1, overflow: TextOverflow.ellipsis)
|
||||
: Text('$stepCount étape${stepCount > 1 ? 's' : ''}', style: TextStyle(color: Colors.grey[600])),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/HtmlText.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_audio_player.dart';
|
||||
@ -130,6 +131,9 @@ class _GuidedPathMapProgressionPageState extends State<GuidedPathMapProgressionP
|
||||
String _translate(List<TranslationDTO>? list) =>
|
||||
TranslationHelper.get(list, widget.visitAppContext);
|
||||
|
||||
String _translatePlain(List<TranslationDTO>? list) =>
|
||||
TranslationHelper.stripHtml(_translate(list));
|
||||
|
||||
bool _hasGeoTrigger(GuidedStepDTO step) =>
|
||||
step.isGeoTriggered == true &&
|
||||
step.geometry != null &&
|
||||
@ -201,13 +205,13 @@ class _GuidedPathMapProgressionPageState extends State<GuidedPathMapProgressionP
|
||||
}
|
||||
|
||||
void _showEnd() {
|
||||
final outro = _stripHtml(
|
||||
final outro = TranslationHelper.stripHtml(
|
||||
TranslationHelper.getWithResource(widget.path.gameMessageFin, widget.visitAppContext));
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (_) => GuidedPathEndView(
|
||||
isGame: _isGame,
|
||||
primaryColor: kMainColor,
|
||||
pathTitle: _translate(widget.path.title),
|
||||
pathTitle: _translatePlain(widget.path.title),
|
||||
stepsCount: _steps.length,
|
||||
estimatedDurationMinutes: widget.path.estimatedDurationMinutes,
|
||||
gameOutro: outro,
|
||||
@ -264,7 +268,7 @@ class _GuidedPathMapProgressionPageState extends State<GuidedPathMapProgressionP
|
||||
final dist = const Distance().distance(position, center);
|
||||
final inZone = dist <= radius;
|
||||
if (inZone && !_inGeoZone) {
|
||||
PushNotificationService.showGeoZoneNotification(stepTitle: _translate(step.title));
|
||||
PushNotificationService.showGeoZoneNotification(stepTitle: _translatePlain(step.title));
|
||||
}
|
||||
setState(() {
|
||||
_distanceMeters = dist;
|
||||
@ -312,12 +316,6 @@ class _GuidedPathMapProgressionPageState extends State<GuidedPathMapProgressionP
|
||||
return '${(m / 1000).toStringAsFixed(m < 10000 ? 1 : 0)} km';
|
||||
}
|
||||
|
||||
String _stripHtml(String html) => html
|
||||
.replaceAll(RegExp(r'<[^>]*>'), ' ')
|
||||
.replaceAll(' ', ' ')
|
||||
.replaceAll(RegExp(r'\s+'), ' ')
|
||||
.trim();
|
||||
|
||||
// ─── Build ────────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
@ -548,7 +546,7 @@ class _GuidedPathMapProgressionPageState extends State<GuidedPathMapProgressionP
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(_translate(widget.path.title),
|
||||
child: Text(_translatePlain(widget.path.title),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
@ -833,7 +831,7 @@ class _GuidedPathMapProgressionPageState extends State<GuidedPathMapProgressionP
|
||||
height: 200, width: double.infinity, fit: BoxFit.cover),
|
||||
),
|
||||
if (step.imageUrl != null) const SizedBox(height: 16),
|
||||
Text(title,
|
||||
HtmlText(title,
|
||||
style: TextStyle(
|
||||
color: _ink,
|
||||
fontSize: 22,
|
||||
|
||||
@ -17,16 +17,10 @@ _Kind _kindOf(QuizQuestion q) {
|
||||
return _Kind.mcq;
|
||||
}
|
||||
|
||||
String _stripHtml(String html) => html
|
||||
.replaceAll(RegExp(r'<[^>]*>'), ' ')
|
||||
.replaceAll(' ', ' ')
|
||||
.replaceAll(RegExp(r'\s+'), ' ')
|
||||
.trim();
|
||||
|
||||
String _normalize(String text) {
|
||||
const accents = 'àâäáãåçèéêëìîïíòôöóõùûüúÿñ';
|
||||
const plain = 'aaaaaaceeeeiiiiooooouuuuyn';
|
||||
var s = _stripHtml(text).toLowerCase();
|
||||
var s = TranslationHelper.stripHtml(text).toLowerCase();
|
||||
final buffer = StringBuffer();
|
||||
for (final ch in s.split('')) {
|
||||
final idx = accents.indexOf(ch);
|
||||
@ -650,7 +644,7 @@ class _StepQuiz extends StatelessWidget {
|
||||
|
||||
Widget _buildSimple(
|
||||
QuizQuestion current, bool hasAnswer, bool checked, bool revealImmediately) {
|
||||
final expectedRaw = _stripHtml(_label(current.responses.first.label));
|
||||
final expectedRaw = TranslationHelper.stripHtml(_label(current.responses.first.label));
|
||||
if (_isDigicode(expectedRaw)) {
|
||||
return _DigicodeField(
|
||||
key: ValueKey('digicode-${current.id}'),
|
||||
|
||||
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
//import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Components/HtmlText.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/flutter_map_view.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/geo_point_filter.dart';
|
||||
@ -95,9 +96,11 @@ class _MapPage extends State<MapPage> {
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.place, color: kMainColor, size: 32),
|
||||
title: Text(title.isNotEmpty ? title : 'Point d\'intérêt'),
|
||||
title: title.isNotEmpty
|
||||
? HtmlText(title)
|
||||
: const Text('Point d\'intérêt'),
|
||||
subtitle: desc.isNotEmpty
|
||||
? Text(desc, maxLines: 2, overflow: TextOverflow.ellipsis)
|
||||
? Text(TranslationHelper.stripHtml(desc), maxLines: 2, overflow: TextOverflow.ellipsis)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
@ -145,7 +148,12 @@ class _MapPage extends State<MapPage> {
|
||||
case MapProvider.MapBox:
|
||||
return MapBoxView(language: appContext.getContext().language, geoPoints: value, mapDTO: mapDTO, icons: widget.icons);
|
||||
default:
|
||||
return GoogleMapView(language: appContext.getContext().language, geoPoints: value, mapDTO: mapDTO!, icons: widget.icons);
|
||||
// Mapbox par défaut : c'est le seul fournisseur dont le SDK sait
|
||||
// empaqueter ses tuiles pour un usage hors ligne (OfflineManager /
|
||||
// TileStore). Google n'expose aucune API de ce genre, et le fond
|
||||
// utilisé ici passe par un endpoint de tuiles non documenté qu'on
|
||||
// n'a de toute façon pas le droit de mettre en cache.
|
||||
return MapBoxView(language: appContext.getContext().language, geoPoints: value, mapDTO: mapDTO, icons: widget.icons);
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -153,7 +161,7 @@ class _MapPage extends State<MapPage> {
|
||||
language: visitAppContext.language!,
|
||||
geoPoints: mapDTO!.points!,
|
||||
categories: mapDTO!.categories!,
|
||||
provider: mapDTO!.mapProvider == null ? MapProvider.Google : mapDTO!.mapProvider!,
|
||||
provider: mapDTO!.mapProvider ?? MapProvider.MapBox,
|
||||
filteredPoints: (value) {
|
||||
_geoPoints.value = value!;
|
||||
}),
|
||||
|
||||
@ -317,7 +317,7 @@ class _MenuPageState extends State<MenuPage> {
|
||||
if (searchValue != null && searchValue!.isNotEmpty) {
|
||||
result = result.where((s) {
|
||||
final rawTitle = TranslationHelper.get(s.title, visitAppContext);
|
||||
final plainText = stripHtmlTags(rawTitle);
|
||||
final plainText = TranslationHelper.stripHtml(rawTitle);
|
||||
final normalizedTitle = removeDiacritics(plainText.toLowerCase());
|
||||
final normalizedSearch = removeDiacritics(searchValue!.toLowerCase());
|
||||
return normalizedTitle.contains(normalizedSearch);
|
||||
@ -328,11 +328,6 @@ class _MenuPageState extends State<MenuPage> {
|
||||
|
||||
filteredSections.value = result;
|
||||
}
|
||||
|
||||
String stripHtmlTags(String htmlText) {
|
||||
final exp = RegExp(r'<[^>]*>', multiLine: true, caseSensitive: false);
|
||||
return htmlText.replaceAll(exp, '');
|
||||
}
|
||||
}
|
||||
|
||||
boxDecoration(AppContext appContext, SectionDTO section, bool isSelected, Object rawSubSectionData) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/HtmlText.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_content_progression_page.dart';
|
||||
@ -312,7 +313,7 @@ class _ParcoursPageState extends State<ParcoursPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
HtmlText(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: _inkDark,
|
||||
@ -324,7 +325,7 @@ class _ParcoursPageState extends State<ParcoursPage> {
|
||||
if (desc.isNotEmpty) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
desc,
|
||||
TranslationHelper.stripHtml(desc),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
@ -432,7 +433,8 @@ class _ParcoursPathStartSheet extends StatelessWidget {
|
||||
(m) => m.language == visitAppContext.language,
|
||||
orElse: () => msg.first,
|
||||
);
|
||||
return found.value ?? '';
|
||||
// Interpolé dans « … » côté affichage : pas de rendu HTML possible.
|
||||
return TranslationHelper.stripHtml(found.value ?? '');
|
||||
}
|
||||
|
||||
@override
|
||||
@ -512,7 +514,7 @@ class _ParcoursPathStartSheet extends StatelessWidget {
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 14,
|
||||
child: Text(
|
||||
child: HtmlText(
|
||||
_title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
@ -547,7 +549,7 @@ class _ParcoursPathStartSheet extends StatelessWidget {
|
||||
if (_desc.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
||||
child: Text(
|
||||
child: HtmlText(
|
||||
_desc,
|
||||
style: const TextStyle(
|
||||
color: _inkMid,
|
||||
@ -645,7 +647,7 @@ class _ParcoursPathStartSheet extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 28),
|
||||
child: Text(
|
||||
child: HtmlText(
|
||||
_title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
|
||||
@ -187,6 +187,12 @@ class _SectionPageState extends State<SectionPage> {
|
||||
List<Map<String, dynamic>> sectionTest = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.sections, sectionId);
|
||||
if(sectionTest.isNotEmpty) {
|
||||
sectionDTO = DatabaseHelper.instance.getSectionFromDB(sectionTest.first);
|
||||
// Le `switch` plus bas construit les sous-DTO à partir de `rawSectionData`.
|
||||
// Hors ligne, il n'était jamais renseigné — `late dynamic` non initialisé,
|
||||
// donc toute ouverture de section levait une LateInitializationError avant
|
||||
// même d'atteindre le rendu. La colonne `data` porte le JSON que la branche
|
||||
// en ligne reçoit de `sectionGetDetail` : c'est exactement la même forme.
|
||||
rawSectionData = jsonDecode(sectionTest.first[DatabaseHelper.columnData] as String);
|
||||
try {
|
||||
SectionRead sectionRead = SectionRead(id: sectionDTO!.id!, readTime: DateTime.now().millisecondsSinceEpoch);
|
||||
await DatabaseHelper.instance.insert(DatabaseTableType.articleRead, sectionRead.toMap());
|
||||
|
||||
@ -16,6 +16,21 @@ import 'package:provider/provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
/// Types dont l'écran ne fait aucun appel réseau au rendu : ils savent s'afficher
|
||||
/// à partir du seul payload stocké en base locale. Les autres restent volontairement
|
||||
/// dehors — Web (webview), Weather (API météo) et Agenda (JSON distant) ne peuvent
|
||||
/// pas fonctionner sans réseau ; Parcours et Event appellent encore l'API au rendu ;
|
||||
/// Map attend un fond de carte hors ligne, sinon le visiteur ouvre une carte grise.
|
||||
const List<SectionType> offlineCapableSectionTypes = [
|
||||
SectionType.Article,
|
||||
SectionType.Quiz,
|
||||
SectionType.Menu,
|
||||
SectionType.Slider,
|
||||
SectionType.Pdf,
|
||||
SectionType.Video,
|
||||
SectionType.Game,
|
||||
];
|
||||
|
||||
class DownloadConfigurationWidget extends StatefulWidget {
|
||||
DownloadConfigurationWidget({Key? key, required this.configuration}) : super(key: key);
|
||||
final ConfigurationDTO configuration;
|
||||
@ -114,6 +129,9 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
||||
// Ressources dont le téléchargement a échoué. Vide = visite complète.
|
||||
final List<String> failedResourceIds = [];
|
||||
|
||||
// Sections dont le payload n'a pas pu être récupéré ou enregistré.
|
||||
final List<String> failedSectionIds = [];
|
||||
|
||||
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;
|
||||
@ -167,17 +185,39 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
||||
if(sections!.isNotEmpty) {
|
||||
|
||||
List<SectionDTO> sectionsInDB = await DatabaseHelper.instance.queryWithConfigurationId(DatabaseTableType.sections, widget.configuration.id!);
|
||||
List<SectionDTO> sectionsToKeep = sections.where((s) => s.type == SectionType.Article || s.type == SectionType.Quiz).toList(); // TODO: supporter tous les types de sections (Game, Menu, Map, PDF, Video, Slider, Web, Weather, Agenda) — actuellement limité à Article et Quiz
|
||||
List<SectionDTO> sectionsToKeep = sections.where((s) => offlineCapableSectionTypes.contains(s.type)).toList();
|
||||
|
||||
sectionsToKeep.sort((a,b) => a.order!.compareTo(b.order!));
|
||||
int newOrder = 0;
|
||||
// Update local DB - Sections
|
||||
for(var section in sectionsToKeep) {
|
||||
section.order = newOrder;
|
||||
|
||||
// Le payload typé n'est pas dans l'export : `ExportConfigurationDTO.sections`
|
||||
// est déserialisé en `SectionDTO`, qui ne porte que l'en-tête commun. C'est
|
||||
// `sectionGetDetail` qui renvoie le JSON complet du sous-type — le même que
|
||||
// celui dont `section_page` se sert en ligne.
|
||||
String? rawSectionJson;
|
||||
try {
|
||||
await DatabaseHelper.instance.insert(DatabaseTableType.sections, ModelsHelper.sectionToMap(section));
|
||||
rawSectionJson = jsonEncode(await visitAppContext.clientAPI.sectionApi!.sectionGetDetail(section.id!));
|
||||
} catch (e) {
|
||||
print("We got an issue inserting section data ${section.id}");
|
||||
debugPrint("[download] détail indisponible pour la section ${section.id} : $e");
|
||||
}
|
||||
|
||||
// Une section sans son payload ne sait rien afficher hors ligne. On préfère
|
||||
// ne pas l'enregistrer et faire échouer la visite, plutôt que laisser le
|
||||
// visiteur ouvrir un article vide au milieu du site.
|
||||
if (rawSectionJson == null) {
|
||||
failedSectionIds.add(section.id ?? '?');
|
||||
newOrder = newOrder + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await DatabaseHelper.instance.insert(DatabaseTableType.sections, ModelsHelper.sectionToMap(section, rawSectionJson));
|
||||
} catch (e) {
|
||||
failedSectionIds.add(section.id ?? '?');
|
||||
debugPrint("[download] échec d'insertion de la section ${section.id} : $e");
|
||||
}
|
||||
|
||||
// Download section image
|
||||
@ -250,10 +290,12 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
||||
// D5 — la visite n'est déclarée téléchargée que si elle l'est vraiment. Le
|
||||
// contenu déjà récupéré reste sur le device : relancer ne re-télécharge que
|
||||
// ce qui manque, `isResourceOutdated` voyant les fichiers présents.
|
||||
if (failedResourceIds.isNotEmpty) {
|
||||
if (failedResourceIds.isNotEmpty || failedSectionIds.isNotEmpty) {
|
||||
debugPrint("[download] ${failedResourceIds.length} ressource(s) en échec : "
|
||||
"${failedResourceIds.join(', ')}");
|
||||
downloadFailureCount.value = failedResourceIds.length;
|
||||
debugPrint("[download] ${failedSectionIds.length} section(s) en échec : "
|
||||
"${failedSectionIds.join(', ')}");
|
||||
downloadFailureCount.value = failedResourceIds.length + failedSectionIds.length;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -43,7 +43,12 @@ List<Translation> translations = [
|
||||
"voice.noQrFound": "Je n'ai pas trouvé de QR code.",
|
||||
"voice.photoCaptured": "Photo prise.",
|
||||
"voice.photoFailed": "Je n'ai pas pu prendre la photo.",
|
||||
"voice.cameraUnavailable": "Je ne peux pas accéder à la caméra."
|
||||
"voice.cameraUnavailable": "Je ne peux pas accéder à la caméra.",
|
||||
"settings.language": "Langue",
|
||||
"settings.notifications": "Notifications push",
|
||||
"settings.voiceAssistant": "Assistant vocal",
|
||||
"settings.voiceAssistantSubtitle": "Lunettes ou micro téléphone",
|
||||
"event.discover": "Découvrir"
|
||||
}),
|
||||
Translation(language: "EN", data: {
|
||||
"visitTitle": "List of tours",
|
||||
@ -87,7 +92,12 @@ List<Translation> translations = [
|
||||
"voice.noQrFound": "I didn't find any QR code.",
|
||||
"voice.photoCaptured": "Photo taken.",
|
||||
"voice.photoFailed": "I couldn't take the photo.",
|
||||
"voice.cameraUnavailable": "I can't access the camera."
|
||||
"voice.cameraUnavailable": "I can't access the camera.",
|
||||
"settings.language": "Language",
|
||||
"settings.notifications": "Push notifications",
|
||||
"settings.voiceAssistant": "Voice assistant",
|
||||
"settings.voiceAssistantSubtitle": "Glasses or phone microphone",
|
||||
"event.discover": "Discover"
|
||||
}),
|
||||
Translation(language: "DE", data: {
|
||||
"visitTitle": "Liste der Touren",
|
||||
@ -131,7 +141,12 @@ List<Translation> translations = [
|
||||
"voice.noQrFound": "Ich habe keinen QR-Code gefunden.",
|
||||
"voice.photoCaptured": "Foto aufgenommen.",
|
||||
"voice.photoFailed": "Ich konnte das Foto nicht aufnehmen.",
|
||||
"voice.cameraUnavailable": "Ich kann nicht auf die Kamera zugreifen."
|
||||
"voice.cameraUnavailable": "Ich kann nicht auf die Kamera zugreifen.",
|
||||
"settings.language": "Sprache",
|
||||
"settings.notifications": "Push-Benachrichtigungen",
|
||||
"settings.voiceAssistant": "Sprachassistent",
|
||||
"settings.voiceAssistantSubtitle": "Brille oder Telefonmikrofon",
|
||||
"event.discover": "Entdecken"
|
||||
}),
|
||||
Translation(language: "NL", data: {
|
||||
"visitTitle": "Lijst met rondleidingen",
|
||||
@ -175,7 +190,12 @@ List<Translation> translations = [
|
||||
"voice.noQrFound": "Ik heb geen QR-code gevonden.",
|
||||
"voice.photoCaptured": "Foto genomen.",
|
||||
"voice.photoFailed": "Ik kon de foto niet nemen.",
|
||||
"voice.cameraUnavailable": "Ik heb geen toegang tot de camera."
|
||||
"voice.cameraUnavailable": "Ik heb geen toegang tot de camera.",
|
||||
"settings.language": "Taal",
|
||||
"settings.notifications": "Pushmeldingen",
|
||||
"settings.voiceAssistant": "Spraakassistent",
|
||||
"settings.voiceAssistantSubtitle": "Bril of telefoonmicrofoon",
|
||||
"event.discover": "Ontdekken"
|
||||
}),
|
||||
Translation(language: "IT", data: {
|
||||
"visitTitle": "Elenco dei tour",
|
||||
@ -219,7 +239,12 @@ List<Translation> translations = [
|
||||
"voice.noQrFound": "Non ho trovato nessun codice QR.",
|
||||
"voice.photoCaptured": "Foto scattata.",
|
||||
"voice.photoFailed": "Non sono riuscito a scattare la foto.",
|
||||
"voice.cameraUnavailable": "Non riesco ad accedere alla fotocamera."
|
||||
"voice.cameraUnavailable": "Non riesco ad accedere alla fotocamera.",
|
||||
"settings.language": "Lingua",
|
||||
"settings.notifications": "Notifiche push",
|
||||
"settings.voiceAssistant": "Assistente vocale",
|
||||
"settings.voiceAssistantSubtitle": "Occhiali o microfono del telefono",
|
||||
"event.discover": "Scopri"
|
||||
}),
|
||||
Translation(language: "ES", data: {
|
||||
"visitTitle": "Lista de recorridos",
|
||||
@ -263,7 +288,12 @@ List<Translation> translations = [
|
||||
"voice.noQrFound": "No he encontrado ningún código QR.",
|
||||
"voice.photoCaptured": "Foto tomada.",
|
||||
"voice.photoFailed": "No he podido tomar la foto.",
|
||||
"voice.cameraUnavailable": "No puedo acceder a la cámara."
|
||||
"voice.cameraUnavailable": "No puedo acceder a la cámara.",
|
||||
"settings.language": "Idioma",
|
||||
"settings.notifications": "Notificaciones push",
|
||||
"settings.voiceAssistant": "Asistente de voz",
|
||||
"settings.voiceAssistantSubtitle": "Gafas o micrófono del teléfono",
|
||||
"event.discover": "Descubrir"
|
||||
}),
|
||||
Translation(language: "PL", data: {
|
||||
"visitTitle": "Lista wycieczek",
|
||||
@ -307,7 +337,12 @@ List<Translation> translations = [
|
||||
"voice.noQrFound": "Nie znalazłem kodu QR.",
|
||||
"voice.photoCaptured": "Zdjęcie zrobione.",
|
||||
"voice.photoFailed": "Nie udało mi się zrobić zdjęcia.",
|
||||
"voice.cameraUnavailable": "Nie mam dostępu do aparatu."
|
||||
"voice.cameraUnavailable": "Nie mam dostępu do aparatu.",
|
||||
"settings.language": "Język",
|
||||
"settings.notifications": "Powiadomienia push",
|
||||
"settings.voiceAssistant": "Asystent głosowy",
|
||||
"settings.voiceAssistantSubtitle": "Okulary lub mikrofon telefonu",
|
||||
"event.discover": "Odkryj"
|
||||
}),
|
||||
Translation(language: "CN", data: {
|
||||
"visitTitle": "旅游清单",
|
||||
@ -351,7 +386,12 @@ List<Translation> translations = [
|
||||
"voice.noQrFound": "我没有找到任何二维码。",
|
||||
"voice.photoCaptured": "照片已拍摄。",
|
||||
"voice.photoFailed": "我无法拍摄照片。",
|
||||
"voice.cameraUnavailable": "我无法访问摄像头。"
|
||||
"voice.cameraUnavailable": "我无法访问摄像头。",
|
||||
"settings.language": "语言",
|
||||
"settings.notifications": "推送通知",
|
||||
"settings.voiceAssistant": "语音助手",
|
||||
"settings.voiceAssistantSubtitle": "眼镜或手机麦克风",
|
||||
"event.discover": "探索"
|
||||
}),
|
||||
Translation(language: "UK", data: {
|
||||
"visitTitle": "Список турів",
|
||||
@ -395,7 +435,12 @@ List<Translation> translations = [
|
||||
"voice.noQrFound": "Я не знайшов жодного QR-коду.",
|
||||
"voice.photoCaptured": "Фото зроблено.",
|
||||
"voice.photoFailed": "Мені не вдалося зробити фото.",
|
||||
"voice.cameraUnavailable": "Я не можу отримати доступ до камери."
|
||||
"voice.cameraUnavailable": "Я не можу отримати доступ до камери.",
|
||||
"settings.language": "Мова",
|
||||
"settings.notifications": "Push-сповіщення",
|
||||
"settings.voiceAssistant": "Голосовий помічник",
|
||||
"settings.voiceAssistantSubtitle": "Окуляри або мікрофон телефона",
|
||||
"event.discover": "Дізнатися більше"
|
||||
}),
|
||||
Translation(language: "AR", data: {
|
||||
"visitTitle": "قائمة الجولات",
|
||||
@ -439,6 +484,11 @@ List<Translation> translations = [
|
||||
"voice.noQrFound": "لم أجد أي رمز QR.",
|
||||
"voice.photoCaptured": "تم التقاط الصورة.",
|
||||
"voice.photoFailed": "لم أتمكن من التقاط الصورة.",
|
||||
"voice.cameraUnavailable": "لا يمكنني الوصول إلى الكاميرا."
|
||||
"voice.cameraUnavailable": "لا يمكنني الوصول إلى الكاميرا.",
|
||||
"settings.language": "اللغة",
|
||||
"settings.notifications": "الإشعارات الفورية",
|
||||
"settings.voiceAssistant": "المساعد الصوتي",
|
||||
"settings.voiceAssistantSubtitle": "النظارات أو ميكروفون الهاتف",
|
||||
"event.discover": "اكتشف"
|
||||
}),
|
||||
];
|
||||
Loading…
x
Reference in New Issue
Block a user