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
|
signingConfig signingConfigs.release
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
debuggable false
|
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:mymuseum_visitapp/constants.dart';
|
||||||
import 'package:speech_to_text/speech_to_text.dart';
|
import 'package:speech_to_text/speech_to_text.dart';
|
||||||
|
|
||||||
String _stripHtml(String html) => html.replaceAll(RegExp(r'<[^>]*>'), '').trim();
|
|
||||||
|
|
||||||
class AssistantChatSheet extends StatefulWidget {
|
class AssistantChatSheet extends StatefulWidget {
|
||||||
final VisitAppContext visitAppContext;
|
final VisitAppContext visitAppContext;
|
||||||
@ -172,7 +171,7 @@ class _AssistantChatSheetState extends State<AssistantChatSheet> {
|
|||||||
String get _venueName {
|
String get _venueName {
|
||||||
final configuration = widget.visitAppContext.configuration;
|
final configuration = widget.visitAppContext.configuration;
|
||||||
if (configuration == null) return '';
|
if (configuration == null) return '';
|
||||||
final title = _stripHtml(
|
final title = TranslationHelper.stripHtml(
|
||||||
TranslationHelper.get(configuration.title, widget.visitAppContext));
|
TranslationHelper.get(configuration.title, widget.visitAppContext));
|
||||||
return title.isNotEmpty ? title : (configuration.label ?? '');
|
return title.isNotEmpty ? title : (configuration.label ?? '');
|
||||||
}
|
}
|
||||||
@ -653,7 +652,7 @@ class _AssistantMessage extends StatelessWidget {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
onNavigate!(
|
onNavigate!(
|
||||||
response.navigation!.sectionId,
|
response.navigation!.sectionId,
|
||||||
_stripHtml(response.navigation!.sectionTitle),
|
TranslationHelper.stripHtml(response.navigation!.sectionTitle),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -700,7 +699,7 @@ class _AssistantMessage extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_stripHtml(response.navigation!.sectionTitle),
|
TranslationHelper.stripHtml(response.navigation!.sectionTitle),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|||||||
@ -8,8 +8,12 @@ import 'package:provider/provider.dart';
|
|||||||
/// Icône circulaire positionnée en haut à droite de l'écran d'accueil.
|
/// Icône circulaire positionnée en haut à droite de l'écran d'accueil.
|
||||||
///
|
///
|
||||||
/// Visible si :
|
/// Visible si :
|
||||||
/// - Les lunettes sont connectées (quel que soit le mode)
|
/// - La fonctionnalité est activée sur l'instance
|
||||||
/// - OU un mode vocal est actif (voiceOnly ou glasses)
|
/// - 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.
|
/// Tap → ouvre VoiceModeSheet.
|
||||||
class GlassesStatusWidget extends StatelessWidget {
|
class GlassesStatusWidget extends StatelessWidget {
|
||||||
@ -29,7 +33,7 @@ class GlassesStatusWidget extends StatelessWidget {
|
|||||||
final modeActive = controller.mode != VoiceMode.none;
|
final modeActive = controller.mode != VoiceMode.none;
|
||||||
final featureAvailable = controller.isFeatureAvailable(visitAppContext);
|
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)) {
|
final Color iconColor = switch ((isConnected, controller.mode)) {
|
||||||
(true, VoiceMode.glasses) => Colors.greenAccent,
|
(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),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Section lunettes — toujours visible
|
// 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(),
|
_GlassesConnectionTile(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Modes — uniquement si isAssistant
|
|
||||||
if (controller.isFeatureAvailable(visitAppContext)) ...[
|
|
||||||
const Text(
|
const Text(
|
||||||
'Mode',
|
'Mode',
|
||||||
style: TextStyle(color: Colors.white54, fontSize: 12, letterSpacing: 1),
|
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';
|
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||||
|
|
||||||
/// Questions proposées au visiteur quand il ouvre l'assistant.
|
/// Questions proposées au visiteur quand il ouvre l'assistant.
|
||||||
@ -69,8 +70,6 @@ class AssistantSuggestions {
|
|||||||
return name == null ? template : template.replaceAll('{name}', name);
|
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) {
|
static String _titleOf(Map<String, dynamic> section, String lang) {
|
||||||
final titles = section['title'];
|
final titles = section['title'];
|
||||||
@ -80,7 +79,7 @@ class AssistantSuggestions {
|
|||||||
orElse: () => titles.first,
|
orElse: () => titles.first,
|
||||||
);
|
);
|
||||||
if (match is! Map) return '';
|
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}) {
|
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 {
|
return {
|
||||||
'id': section.id,
|
'id': section.id,
|
||||||
'instanceId': section.instanceId,
|
'instanceId': section.instanceId,
|
||||||
@ -33,7 +39,7 @@ class ModelsHelper {
|
|||||||
'isSubSection': section.isSubSection,
|
'isSubSection': section.isSubSection,
|
||||||
'parentId': section.parentId,
|
'parentId': section.parentId,
|
||||||
'type': section.type!.value,
|
'type': section.type!.value,
|
||||||
//'data': section.data, // TODO section data
|
'data': rawSectionJson,
|
||||||
'dateCreation': section.dateCreation!.toUtc().toIso8601String(),
|
'dateCreation': section.dateCreation!.toUtc().toIso8601String(),
|
||||||
'orderOfElement': section.order,
|
'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) {
|
static String getFromLocale(String valueToGet, VisitAppContext visitAppContext) {
|
||||||
try {
|
try {
|
||||||
return translations.where((element) => element.language == visitAppContext.language).first.data![valueToGet]!;
|
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) {
|
if (searchValue != null && searchValue!.isNotEmpty) {
|
||||||
result = result.where((s) =>
|
result = result.where((s) =>
|
||||||
removeDiacritics(TranslationHelper.get(s.title, visitAppContext).toLowerCase())
|
removeDiacritics(TranslationHelper.getPlain(s.title, visitAppContext).toLowerCase())
|
||||||
.contains(removeDiacritics(searchValue!.toLowerCase()))
|
.contains(removeDiacritics(searchValue!.toLowerCase()))
|
||||||
).toList();
|
).toList();
|
||||||
} else if (searchNumberValue != null) {
|
} else if (searchNumberValue != null) {
|
||||||
|
|||||||
@ -207,7 +207,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (matches.isNotEmpty) {
|
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) {
|
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,
|
alignment: Alignment.bottomRight,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 90, bottom: 1),
|
padding: const EdgeInsets.only(right: 90, bottom: 1),
|
||||||
|
|||||||
@ -140,15 +140,19 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Title at bottom-left — hauteur bornée + clip : empêche le débordement
|
// Titre + chevron sur une même ligne : le chevron reste centré
|
||||||
// même si le moteur HTML natif ignore -webkit-line-clamp (contenu Quill).
|
// verticalement sur le bloc de titre, quel que soit son nombre de lignes.
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
left: 10,
|
left: 10,
|
||||||
right: 28,
|
right: 8,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
child: ClipRect(
|
child: ClipRect(
|
||||||
child: SizedBox(
|
child: ConstrainedBox(
|
||||||
height: 40,
|
constraints: const BoxConstraints(maxHeight: 40),
|
||||||
child: HtmlWidget(
|
child: HtmlWidget(
|
||||||
cleanedTitle,
|
cleanedTitle,
|
||||||
textStyle: const TextStyle(
|
textStyle: const TextStyle(
|
||||||
@ -159,17 +163,17 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
customStylesBuilder: (_) => {
|
customStylesBuilder: (_) => {
|
||||||
'font-family': 'Roboto',
|
'font-family': 'Roboto',
|
||||||
|
'margin': '0',
|
||||||
'-webkit-line-clamp': '2',
|
'-webkit-line-clamp': '2',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Chevron
|
const SizedBox(width: 4),
|
||||||
const Positioned(
|
const Icon(Icons.chevron_right, size: 18, color: Colors.white),
|
||||||
bottom: 10,
|
],
|
||||||
right: 8,
|
),
|
||||||
child: Icon(Icons.chevron_right, size: 18, color: Colors.white),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -211,23 +215,33 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
// Entrée assistant vocal (si feature disponible)
|
// Entrée assistant vocal (si feature disponible)
|
||||||
if (visitAppContext.applicationInstanceDTO?.isAssistant == true) ...[
|
if (visitAppContext.applicationInstanceDTO?.isAssistant == true) ...[
|
||||||
ListTile(
|
Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
leading: const Icon(Icons.mic, color: kMainColor1),
|
leading: const Icon(Icons.mic, color: kMainColor1),
|
||||||
title: const Text('Assistant vocal', style: TextStyle(fontWeight: FontWeight.w600)),
|
title: Text(
|
||||||
subtitle: const Text('Lunettes ou micro téléphone'),
|
TranslationHelper.getFromLocale('settings.voiceAssistant', ctx2),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
subtitle: Text(TranslationHelper.getFromLocale('settings.voiceAssistantSubtitle', ctx2)),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
VoiceModeSheet.show(ctx, visitAppContext: visitAppContext);
|
VoiceModeSheet.show(ctx, visitAppContext: visitAppContext);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const Divider(),
|
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),
|
const SizedBox(height: 12),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 10,
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
children: configLanguages.map((lang) {
|
children: configLanguages.map((lang) {
|
||||||
final isSelected = ctx2.language == lang;
|
final isSelected = ctx2.language == lang;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@ -245,7 +259,7 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: isSelected ? Border.all(color: kMainColor, width: 2.5) : null,
|
border: isSelected ? Border.all(color: kMainColor, width: 2.5) : null,
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.cover,
|
||||||
image: AssetImage('assets/images/old/${lang.toLowerCase()}.png'),
|
image: AssetImage('assets/images/old/${lang.toLowerCase()}.png'),
|
||||||
),
|
),
|
||||||
boxShadow: const [BoxShadow(color: kSecondGrey, spreadRadius: 0.5, blurRadius: 5, offset: Offset(0, 1.5))],
|
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(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
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(
|
Switch(
|
||||||
value: ctx2.notificationsEnabled,
|
value: ctx2.notificationsEnabled,
|
||||||
activeThumbColor: kMainColor,
|
activeThumbColor: kMainColor,
|
||||||
@ -398,16 +415,25 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (titleEntry != null)
|
if (titleEntry != null)
|
||||||
Text(
|
ClipRect(
|
||||||
titleEntry.value ?? '',
|
child: ConstrainedBox(
|
||||||
style: const TextStyle(
|
constraints: const BoxConstraints(maxHeight: 52),
|
||||||
|
child: HtmlWidget(
|
||||||
|
(titleEntry.value ?? '')
|
||||||
|
.replaceAll('\n', ' ')
|
||||||
|
.replaceAll('<br>', ' '),
|
||||||
|
textStyle: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
height: 1.2,
|
height: 1.2,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
customStylesBuilder: (_) => {
|
||||||
overflow: TextOverflow.ellipsis,
|
'margin': '0',
|
||||||
|
'-webkit-line-clamp': '2',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
@ -443,7 +469,7 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Découvrir',
|
TranslationHelper.getFromLocale('event.discover', visitAppContext),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: kMainColor,
|
color: kMainColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:manager_api_new/api.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/Helpers/translationHelper.dart';
|
||||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||||
import 'package:mymuseum_visitapp/Screens/Sections/Event/event_map_full_page.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) {
|
Widget _buildHero(BuildContext context) {
|
||||||
final title = _safeTranslate(widget.section.title);
|
final title = _safeTranslate(widget.section.title);
|
||||||
|
final titlePlain = _safeTranslatePlain(widget.section.title);
|
||||||
final dateRange = _formatDateRange();
|
final dateRange = _formatDateRange();
|
||||||
final expandedHeight = MediaQuery.of(context).size.height * 0.52;
|
final expandedHeight = MediaQuery.of(context).size.height * 0.52;
|
||||||
|
|
||||||
@ -117,7 +119,7 @@ class _EventPageState extends State<EventPage> {
|
|||||||
pinned: true,
|
pinned: true,
|
||||||
backgroundColor: kMainColor,
|
backgroundColor: kMainColor,
|
||||||
title: Text(
|
title: Text(
|
||||||
title,
|
titlePlain,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@ -163,7 +165,7 @@ class _EventPageState extends State<EventPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
HtmlText(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@ -171,8 +173,6 @@ class _EventPageState extends State<EventPage> {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
height: 1.2,
|
height: 1.2,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
if (dateRange.isNotEmpty) ...[
|
if (dateRange.isNotEmpty) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@ -292,7 +292,7 @@ class _EventPageState extends State<EventPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
HtmlText(
|
||||||
title,
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isActive ? Colors.white : Colors.white.withOpacity(0.9),
|
color: isActive ? Colors.white : Colors.white.withOpacity(0.9),
|
||||||
@ -370,7 +370,7 @@ class _EventPageState extends State<EventPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
HtmlText(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@ -380,7 +380,7 @@ class _EventPageState extends State<EventPage> {
|
|||||||
),
|
),
|
||||||
if (desc.isNotEmpty) ...[
|
if (desc.isNotEmpty) ...[
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
HtmlText(
|
||||||
desc,
|
desc,
|
||||||
style: TextStyle(color: Colors.grey[400], fontSize: 14, height: 1.6),
|
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),
|
Icon(Icons.place, color: kMainColor, size: 16),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: HtmlText(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||||
),
|
),
|
||||||
@ -614,7 +614,7 @@ class _EventPageState extends State<EventPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildParcoursChip(BuildContext context, GuidedPathDTO path) {
|
Widget _buildParcoursChip(BuildContext context, GuidedPathDTO path) {
|
||||||
final title = _safeTranslate(path.title);
|
final title = _safeTranslatePlain(path.title);
|
||||||
final stepCount = path.steps?.length ?? 0;
|
final stepCount = path.steps?.length ?? 0;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@ -700,7 +700,7 @@ class _EventPageState extends State<EventPage> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
..._paths.map((path) {
|
..._paths.map((path) {
|
||||||
final title = _safeTranslate(path.title);
|
final title = _safeTranslate(path.title);
|
||||||
final desc = _safeTranslate(path.description);
|
final desc = _safeTranslatePlain(path.description);
|
||||||
final stepCount = path.steps?.length ?? 0;
|
final stepCount = path.steps?.length ?? 0;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
@ -713,7 +713,7 @@ class _EventPageState extends State<EventPage> {
|
|||||||
),
|
),
|
||||||
child: Icon(Icons.map_outlined, color: kMainColor, size: 20),
|
child: Icon(Icons.map_outlined, color: kMainColor, size: 20),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: HtmlText(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500),
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
@ -766,4 +766,9 @@ class _EventPageState extends State<EventPage> {
|
|||||||
return list.first.value ?? '';
|
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:manager_api_new/api.dart';
|
||||||
|
|
||||||
import 'package:mymuseum_visitapp/Components/SliderImages.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/Helpers/translationHelper.dart';
|
||||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||||
@ -124,6 +125,9 @@ class _GuidedPathContentProgressionPageState
|
|||||||
String _translate(List<TranslationDTO>? list) =>
|
String _translate(List<TranslationDTO>? list) =>
|
||||||
TranslationHelper.get(list, widget.visitAppContext);
|
TranslationHelper.get(list, widget.visitAppContext);
|
||||||
|
|
||||||
|
String _translatePlain(List<TranslationDTO>? list) =>
|
||||||
|
TranslationHelper.stripHtml(_translate(list));
|
||||||
|
|
||||||
bool get _canAdvance => _challenge?.canAdvance ?? true;
|
bool get _canAdvance => _challenge?.canAdvance ?? true;
|
||||||
|
|
||||||
void _advance() {
|
void _advance() {
|
||||||
@ -213,13 +217,13 @@ class _GuidedPathContentProgressionPageState
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showEnd() {
|
void _showEnd() {
|
||||||
final outro = _stripHtml(
|
final outro = TranslationHelper.stripHtml(
|
||||||
TranslationHelper.getWithResource(widget.path.gameMessageFin, widget.visitAppContext));
|
TranslationHelper.getWithResource(widget.path.gameMessageFin, widget.visitAppContext));
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
Navigator.of(context).push(MaterialPageRoute(
|
||||||
builder: (_) => GuidedPathEndView(
|
builder: (_) => GuidedPathEndView(
|
||||||
isGame: _isGame,
|
isGame: _isGame,
|
||||||
primaryColor: kMainColor,
|
primaryColor: kMainColor,
|
||||||
pathTitle: _translate(widget.path.title),
|
pathTitle: _translatePlain(widget.path.title),
|
||||||
stepsCount: _steps.length,
|
stepsCount: _steps.length,
|
||||||
estimatedDurationMinutes: widget.path.estimatedDurationMinutes,
|
estimatedDurationMinutes: widget.path.estimatedDurationMinutes,
|
||||||
gameOutro: outro,
|
gameOutro: outro,
|
||||||
@ -231,12 +235,6 @@ class _GuidedPathContentProgressionPageState
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
String _stripHtml(String html) => html
|
|
||||||
.replaceAll(RegExp(r'<[^>]*>'), ' ')
|
|
||||||
.replaceAll(' ', ' ')
|
|
||||||
.replaceAll(RegExp(r'\s+'), ' ')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final step = _currentStep;
|
final step = _currentStep;
|
||||||
@ -389,7 +387,7 @@ class _GuidedPathContentProgressionPageState
|
|||||||
height: 200, width: double.infinity, fit: BoxFit.cover),
|
height: 200, width: double.infinity, fit: BoxFit.cover),
|
||||||
),
|
),
|
||||||
if (hasContents || step.imageUrl != null) const SizedBox(height: 16),
|
if (hasContents || step.imageUrl != null) const SizedBox(height: 16),
|
||||||
Text(title,
|
HtmlText(title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _ink, fontSize: 22, fontWeight: FontWeight.w600, height: 1.15)),
|
color: _ink, fontSize: 22, fontWeight: FontWeight.w600, height: 1.15)),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@ -399,7 +397,7 @@ class _GuidedPathContentProgressionPageState
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: _isGame ? _gold.withOpacity(0.12) : _accent.withOpacity(0.1),
|
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(
|
style: TextStyle(
|
||||||
color: _accent,
|
color: _accent,
|
||||||
fontSize: 11.5,
|
fontSize: 11.5,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:manager_api_new/api.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/Helpers/translationHelper.dart';
|
||||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||||
import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_map_progression_page.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),
|
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
|
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])),
|
: Text('$stepCount étape${stepCount > 1 ? 's' : ''}', style: TextStyle(color: Colors.grey[600])),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
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:geolocator/geolocator.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:manager_api_new/api.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/Helpers/translationHelper.dart';
|
||||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||||
import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_audio_player.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) =>
|
String _translate(List<TranslationDTO>? list) =>
|
||||||
TranslationHelper.get(list, widget.visitAppContext);
|
TranslationHelper.get(list, widget.visitAppContext);
|
||||||
|
|
||||||
|
String _translatePlain(List<TranslationDTO>? list) =>
|
||||||
|
TranslationHelper.stripHtml(_translate(list));
|
||||||
|
|
||||||
bool _hasGeoTrigger(GuidedStepDTO step) =>
|
bool _hasGeoTrigger(GuidedStepDTO step) =>
|
||||||
step.isGeoTriggered == true &&
|
step.isGeoTriggered == true &&
|
||||||
step.geometry != null &&
|
step.geometry != null &&
|
||||||
@ -201,13 +205,13 @@ class _GuidedPathMapProgressionPageState extends State<GuidedPathMapProgressionP
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showEnd() {
|
void _showEnd() {
|
||||||
final outro = _stripHtml(
|
final outro = TranslationHelper.stripHtml(
|
||||||
TranslationHelper.getWithResource(widget.path.gameMessageFin, widget.visitAppContext));
|
TranslationHelper.getWithResource(widget.path.gameMessageFin, widget.visitAppContext));
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
Navigator.of(context).push(MaterialPageRoute(
|
||||||
builder: (_) => GuidedPathEndView(
|
builder: (_) => GuidedPathEndView(
|
||||||
isGame: _isGame,
|
isGame: _isGame,
|
||||||
primaryColor: kMainColor,
|
primaryColor: kMainColor,
|
||||||
pathTitle: _translate(widget.path.title),
|
pathTitle: _translatePlain(widget.path.title),
|
||||||
stepsCount: _steps.length,
|
stepsCount: _steps.length,
|
||||||
estimatedDurationMinutes: widget.path.estimatedDurationMinutes,
|
estimatedDurationMinutes: widget.path.estimatedDurationMinutes,
|
||||||
gameOutro: outro,
|
gameOutro: outro,
|
||||||
@ -264,7 +268,7 @@ class _GuidedPathMapProgressionPageState extends State<GuidedPathMapProgressionP
|
|||||||
final dist = const Distance().distance(position, center);
|
final dist = const Distance().distance(position, center);
|
||||||
final inZone = dist <= radius;
|
final inZone = dist <= radius;
|
||||||
if (inZone && !_inGeoZone) {
|
if (inZone && !_inGeoZone) {
|
||||||
PushNotificationService.showGeoZoneNotification(stepTitle: _translate(step.title));
|
PushNotificationService.showGeoZoneNotification(stepTitle: _translatePlain(step.title));
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_distanceMeters = dist;
|
_distanceMeters = dist;
|
||||||
@ -312,12 +316,6 @@ class _GuidedPathMapProgressionPageState extends State<GuidedPathMapProgressionP
|
|||||||
return '${(m / 1000).toStringAsFixed(m < 10000 ? 1 : 0)} km';
|
return '${(m / 1000).toStringAsFixed(m < 10000 ? 1 : 0)} km';
|
||||||
}
|
}
|
||||||
|
|
||||||
String _stripHtml(String html) => html
|
|
||||||
.replaceAll(RegExp(r'<[^>]*>'), ' ')
|
|
||||||
.replaceAll(' ', ' ')
|
|
||||||
.replaceAll(RegExp(r'\s+'), ' ')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
// ─── Build ────────────────────────────────────────────────────────────────
|
// ─── Build ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -548,7 +546,7 @@ class _GuidedPathMapProgressionPageState extends State<GuidedPathMapProgressionP
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(_translate(widget.path.title),
|
child: Text(_translatePlain(widget.path.title),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -833,7 +831,7 @@ class _GuidedPathMapProgressionPageState extends State<GuidedPathMapProgressionP
|
|||||||
height: 200, width: double.infinity, fit: BoxFit.cover),
|
height: 200, width: double.infinity, fit: BoxFit.cover),
|
||||||
),
|
),
|
||||||
if (step.imageUrl != null) const SizedBox(height: 16),
|
if (step.imageUrl != null) const SizedBox(height: 16),
|
||||||
Text(title,
|
HtmlText(title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _ink,
|
color: _ink,
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
|
|||||||
@ -17,16 +17,10 @@ _Kind _kindOf(QuizQuestion q) {
|
|||||||
return _Kind.mcq;
|
return _Kind.mcq;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _stripHtml(String html) => html
|
|
||||||
.replaceAll(RegExp(r'<[^>]*>'), ' ')
|
|
||||||
.replaceAll(' ', ' ')
|
|
||||||
.replaceAll(RegExp(r'\s+'), ' ')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
String _normalize(String text) {
|
String _normalize(String text) {
|
||||||
const accents = 'àâäáãåçèéêëìîïíòôöóõùûüúÿñ';
|
const accents = 'àâäáãåçèéêëìîïíòôöóõùûüúÿñ';
|
||||||
const plain = 'aaaaaaceeeeiiiiooooouuuuyn';
|
const plain = 'aaaaaaceeeeiiiiooooouuuuyn';
|
||||||
var s = _stripHtml(text).toLowerCase();
|
var s = TranslationHelper.stripHtml(text).toLowerCase();
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
for (final ch in s.split('')) {
|
for (final ch in s.split('')) {
|
||||||
final idx = accents.indexOf(ch);
|
final idx = accents.indexOf(ch);
|
||||||
@ -650,7 +644,7 @@ class _StepQuiz extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildSimple(
|
Widget _buildSimple(
|
||||||
QuizQuestion current, bool hasAnswer, bool checked, bool revealImmediately) {
|
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)) {
|
if (_isDigicode(expectedRaw)) {
|
||||||
return _DigicodeField(
|
return _DigicodeField(
|
||||||
key: ValueKey('digicode-${current.id}'),
|
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:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'package:manager_api_new/api.dart';
|
import 'package:manager_api_new/api.dart';
|
||||||
import 'package:mymuseum_visitapp/Models/visitContext.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/Helpers/translationHelper.dart';
|
||||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/flutter_map_view.dart';
|
import 'package:mymuseum_visitapp/Screens/Sections/Map/flutter_map_view.dart';
|
||||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/geo_point_filter.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),
|
: 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
|
subtitle: desc.isNotEmpty
|
||||||
? Text(desc, maxLines: 2, overflow: TextOverflow.ellipsis)
|
? Text(TranslationHelper.stripHtml(desc), maxLines: 2, overflow: TextOverflow.ellipsis)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -145,7 +148,12 @@ class _MapPage extends State<MapPage> {
|
|||||||
case MapProvider.MapBox:
|
case MapProvider.MapBox:
|
||||||
return MapBoxView(language: appContext.getContext().language, geoPoints: value, mapDTO: mapDTO, icons: widget.icons);
|
return MapBoxView(language: appContext.getContext().language, geoPoints: value, mapDTO: mapDTO, icons: widget.icons);
|
||||||
default:
|
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!,
|
language: visitAppContext.language!,
|
||||||
geoPoints: mapDTO!.points!,
|
geoPoints: mapDTO!.points!,
|
||||||
categories: mapDTO!.categories!,
|
categories: mapDTO!.categories!,
|
||||||
provider: mapDTO!.mapProvider == null ? MapProvider.Google : mapDTO!.mapProvider!,
|
provider: mapDTO!.mapProvider ?? MapProvider.MapBox,
|
||||||
filteredPoints: (value) {
|
filteredPoints: (value) {
|
||||||
_geoPoints.value = value!;
|
_geoPoints.value = value!;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -317,7 +317,7 @@ class _MenuPageState extends State<MenuPage> {
|
|||||||
if (searchValue != null && searchValue!.isNotEmpty) {
|
if (searchValue != null && searchValue!.isNotEmpty) {
|
||||||
result = result.where((s) {
|
result = result.where((s) {
|
||||||
final rawTitle = TranslationHelper.get(s.title, visitAppContext);
|
final rawTitle = TranslationHelper.get(s.title, visitAppContext);
|
||||||
final plainText = stripHtmlTags(rawTitle);
|
final plainText = TranslationHelper.stripHtml(rawTitle);
|
||||||
final normalizedTitle = removeDiacritics(plainText.toLowerCase());
|
final normalizedTitle = removeDiacritics(plainText.toLowerCase());
|
||||||
final normalizedSearch = removeDiacritics(searchValue!.toLowerCase());
|
final normalizedSearch = removeDiacritics(searchValue!.toLowerCase());
|
||||||
return normalizedTitle.contains(normalizedSearch);
|
return normalizedTitle.contains(normalizedSearch);
|
||||||
@ -328,11 +328,6 @@ class _MenuPageState extends State<MenuPage> {
|
|||||||
|
|
||||||
filteredSections.value = result;
|
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) {
|
boxDecoration(AppContext appContext, SectionDTO section, bool isSelected, Object rawSubSectionData) {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:manager_api_new/api.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/Helpers/translationHelper.dart';
|
||||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||||
import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_content_progression_page.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),
|
const SizedBox(height: 6),
|
||||||
Text(
|
HtmlText(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: _inkDark,
|
color: _inkDark,
|
||||||
@ -324,7 +325,7 @@ class _ParcoursPageState extends State<ParcoursPage> {
|
|||||||
if (desc.isNotEmpty) ...[
|
if (desc.isNotEmpty) ...[
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(
|
Text(
|
||||||
desc,
|
TranslationHelper.stripHtml(desc),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -432,7 +433,8 @@ class _ParcoursPathStartSheet extends StatelessWidget {
|
|||||||
(m) => m.language == visitAppContext.language,
|
(m) => m.language == visitAppContext.language,
|
||||||
orElse: () => msg.first,
|
orElse: () => msg.first,
|
||||||
);
|
);
|
||||||
return found.value ?? '';
|
// Interpolé dans « … » côté affichage : pas de rendu HTML possible.
|
||||||
|
return TranslationHelper.stripHtml(found.value ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -512,7 +514,7 @@ class _ParcoursPathStartSheet extends StatelessWidget {
|
|||||||
left: 16,
|
left: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
bottom: 14,
|
bottom: 14,
|
||||||
child: Text(
|
child: HtmlText(
|
||||||
_title,
|
_title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@ -547,7 +549,7 @@ class _ParcoursPathStartSheet extends StatelessWidget {
|
|||||||
if (_desc.isNotEmpty)
|
if (_desc.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
||||||
child: Text(
|
child: HtmlText(
|
||||||
_desc,
|
_desc,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: _inkMid,
|
color: _inkMid,
|
||||||
@ -645,7 +647,7 @@ class _ParcoursPathStartSheet extends StatelessWidget {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 28),
|
padding: const EdgeInsets.symmetric(horizontal: 28),
|
||||||
child: Text(
|
child: HtmlText(
|
||||||
_title,
|
_title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
|||||||
@ -187,6 +187,12 @@ class _SectionPageState extends State<SectionPage> {
|
|||||||
List<Map<String, dynamic>> sectionTest = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.sections, sectionId);
|
List<Map<String, dynamic>> sectionTest = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.sections, sectionId);
|
||||||
if(sectionTest.isNotEmpty) {
|
if(sectionTest.isNotEmpty) {
|
||||||
sectionDTO = DatabaseHelper.instance.getSectionFromDB(sectionTest.first);
|
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 {
|
try {
|
||||||
SectionRead sectionRead = SectionRead(id: sectionDTO!.id!, readTime: DateTime.now().millisecondsSinceEpoch);
|
SectionRead sectionRead = SectionRead(id: sectionDTO!.id!, readTime: DateTime.now().millisecondsSinceEpoch);
|
||||||
await DatabaseHelper.instance.insert(DatabaseTableType.articleRead, sectionRead.toMap());
|
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:http/http.dart' as http;
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
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 {
|
class DownloadConfigurationWidget extends StatefulWidget {
|
||||||
DownloadConfigurationWidget({Key? key, required this.configuration}) : super(key: key);
|
DownloadConfigurationWidget({Key? key, required this.configuration}) : super(key: key);
|
||||||
final ConfigurationDTO configuration;
|
final ConfigurationDTO configuration;
|
||||||
@ -114,6 +129,9 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
|||||||
// Ressources dont le téléchargement a échoué. Vide = visite complète.
|
// Ressources dont le téléchargement a échoué. Vide = visite complète.
|
||||||
final List<String> failedResourceIds = [];
|
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]));
|
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;
|
currentResourceNbr.value = resourcesToDownload.length;
|
||||||
@ -167,17 +185,39 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
|||||||
if(sections!.isNotEmpty) {
|
if(sections!.isNotEmpty) {
|
||||||
|
|
||||||
List<SectionDTO> sectionsInDB = await DatabaseHelper.instance.queryWithConfigurationId(DatabaseTableType.sections, widget.configuration.id!);
|
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!));
|
sectionsToKeep.sort((a,b) => a.order!.compareTo(b.order!));
|
||||||
int newOrder = 0;
|
int newOrder = 0;
|
||||||
// Update local DB - Sections
|
// Update local DB - Sections
|
||||||
for(var section in sectionsToKeep) {
|
for(var section in sectionsToKeep) {
|
||||||
section.order = newOrder;
|
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 {
|
try {
|
||||||
await DatabaseHelper.instance.insert(DatabaseTableType.sections, ModelsHelper.sectionToMap(section));
|
rawSectionJson = jsonEncode(await visitAppContext.clientAPI.sectionApi!.sectionGetDetail(section.id!));
|
||||||
} catch (e) {
|
} 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
|
// 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
|
// 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
|
// 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.
|
// 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 : "
|
debugPrint("[download] ${failedResourceIds.length} ressource(s) en échec : "
|
||||||
"${failedResourceIds.join(', ')}");
|
"${failedResourceIds.join(', ')}");
|
||||||
downloadFailureCount.value = failedResourceIds.length;
|
debugPrint("[download] ${failedSectionIds.length} section(s) en échec : "
|
||||||
|
"${failedSectionIds.join(', ')}");
|
||||||
|
downloadFailureCount.value = failedResourceIds.length + failedSectionIds.length;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,12 @@ List<Translation> translations = [
|
|||||||
"voice.noQrFound": "Je n'ai pas trouvé de QR code.",
|
"voice.noQrFound": "Je n'ai pas trouvé de QR code.",
|
||||||
"voice.photoCaptured": "Photo prise.",
|
"voice.photoCaptured": "Photo prise.",
|
||||||
"voice.photoFailed": "Je n'ai pas pu prendre la photo.",
|
"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: {
|
Translation(language: "EN", data: {
|
||||||
"visitTitle": "List of tours",
|
"visitTitle": "List of tours",
|
||||||
@ -87,7 +92,12 @@ List<Translation> translations = [
|
|||||||
"voice.noQrFound": "I didn't find any QR code.",
|
"voice.noQrFound": "I didn't find any QR code.",
|
||||||
"voice.photoCaptured": "Photo taken.",
|
"voice.photoCaptured": "Photo taken.",
|
||||||
"voice.photoFailed": "I couldn't take the photo.",
|
"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: {
|
Translation(language: "DE", data: {
|
||||||
"visitTitle": "Liste der Touren",
|
"visitTitle": "Liste der Touren",
|
||||||
@ -131,7 +141,12 @@ List<Translation> translations = [
|
|||||||
"voice.noQrFound": "Ich habe keinen QR-Code gefunden.",
|
"voice.noQrFound": "Ich habe keinen QR-Code gefunden.",
|
||||||
"voice.photoCaptured": "Foto aufgenommen.",
|
"voice.photoCaptured": "Foto aufgenommen.",
|
||||||
"voice.photoFailed": "Ich konnte das Foto nicht aufnehmen.",
|
"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: {
|
Translation(language: "NL", data: {
|
||||||
"visitTitle": "Lijst met rondleidingen",
|
"visitTitle": "Lijst met rondleidingen",
|
||||||
@ -175,7 +190,12 @@ List<Translation> translations = [
|
|||||||
"voice.noQrFound": "Ik heb geen QR-code gevonden.",
|
"voice.noQrFound": "Ik heb geen QR-code gevonden.",
|
||||||
"voice.photoCaptured": "Foto genomen.",
|
"voice.photoCaptured": "Foto genomen.",
|
||||||
"voice.photoFailed": "Ik kon de foto niet nemen.",
|
"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: {
|
Translation(language: "IT", data: {
|
||||||
"visitTitle": "Elenco dei tour",
|
"visitTitle": "Elenco dei tour",
|
||||||
@ -219,7 +239,12 @@ List<Translation> translations = [
|
|||||||
"voice.noQrFound": "Non ho trovato nessun codice QR.",
|
"voice.noQrFound": "Non ho trovato nessun codice QR.",
|
||||||
"voice.photoCaptured": "Foto scattata.",
|
"voice.photoCaptured": "Foto scattata.",
|
||||||
"voice.photoFailed": "Non sono riuscito a scattare la foto.",
|
"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: {
|
Translation(language: "ES", data: {
|
||||||
"visitTitle": "Lista de recorridos",
|
"visitTitle": "Lista de recorridos",
|
||||||
@ -263,7 +288,12 @@ List<Translation> translations = [
|
|||||||
"voice.noQrFound": "No he encontrado ningún código QR.",
|
"voice.noQrFound": "No he encontrado ningún código QR.",
|
||||||
"voice.photoCaptured": "Foto tomada.",
|
"voice.photoCaptured": "Foto tomada.",
|
||||||
"voice.photoFailed": "No he podido tomar la foto.",
|
"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: {
|
Translation(language: "PL", data: {
|
||||||
"visitTitle": "Lista wycieczek",
|
"visitTitle": "Lista wycieczek",
|
||||||
@ -307,7 +337,12 @@ List<Translation> translations = [
|
|||||||
"voice.noQrFound": "Nie znalazłem kodu QR.",
|
"voice.noQrFound": "Nie znalazłem kodu QR.",
|
||||||
"voice.photoCaptured": "Zdjęcie zrobione.",
|
"voice.photoCaptured": "Zdjęcie zrobione.",
|
||||||
"voice.photoFailed": "Nie udało mi się zrobić zdjęcia.",
|
"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: {
|
Translation(language: "CN", data: {
|
||||||
"visitTitle": "旅游清单",
|
"visitTitle": "旅游清单",
|
||||||
@ -351,7 +386,12 @@ List<Translation> translations = [
|
|||||||
"voice.noQrFound": "我没有找到任何二维码。",
|
"voice.noQrFound": "我没有找到任何二维码。",
|
||||||
"voice.photoCaptured": "照片已拍摄。",
|
"voice.photoCaptured": "照片已拍摄。",
|
||||||
"voice.photoFailed": "我无法拍摄照片。",
|
"voice.photoFailed": "我无法拍摄照片。",
|
||||||
"voice.cameraUnavailable": "我无法访问摄像头。"
|
"voice.cameraUnavailable": "我无法访问摄像头。",
|
||||||
|
"settings.language": "语言",
|
||||||
|
"settings.notifications": "推送通知",
|
||||||
|
"settings.voiceAssistant": "语音助手",
|
||||||
|
"settings.voiceAssistantSubtitle": "眼镜或手机麦克风",
|
||||||
|
"event.discover": "探索"
|
||||||
}),
|
}),
|
||||||
Translation(language: "UK", data: {
|
Translation(language: "UK", data: {
|
||||||
"visitTitle": "Список турів",
|
"visitTitle": "Список турів",
|
||||||
@ -395,7 +435,12 @@ List<Translation> translations = [
|
|||||||
"voice.noQrFound": "Я не знайшов жодного QR-коду.",
|
"voice.noQrFound": "Я не знайшов жодного QR-коду.",
|
||||||
"voice.photoCaptured": "Фото зроблено.",
|
"voice.photoCaptured": "Фото зроблено.",
|
||||||
"voice.photoFailed": "Мені не вдалося зробити фото.",
|
"voice.photoFailed": "Мені не вдалося зробити фото.",
|
||||||
"voice.cameraUnavailable": "Я не можу отримати доступ до камери."
|
"voice.cameraUnavailable": "Я не можу отримати доступ до камери.",
|
||||||
|
"settings.language": "Мова",
|
||||||
|
"settings.notifications": "Push-сповіщення",
|
||||||
|
"settings.voiceAssistant": "Голосовий помічник",
|
||||||
|
"settings.voiceAssistantSubtitle": "Окуляри або мікрофон телефона",
|
||||||
|
"event.discover": "Дізнатися більше"
|
||||||
}),
|
}),
|
||||||
Translation(language: "AR", data: {
|
Translation(language: "AR", data: {
|
||||||
"visitTitle": "قائمة الجولات",
|
"visitTitle": "قائمة الجولات",
|
||||||
@ -439,6 +484,11 @@ List<Translation> translations = [
|
|||||||
"voice.noQrFound": "لم أجد أي رمز QR.",
|
"voice.noQrFound": "لم أجد أي رمز QR.",
|
||||||
"voice.photoCaptured": "تم التقاط الصورة.",
|
"voice.photoCaptured": "تم التقاط الصورة.",
|
||||||
"voice.photoFailed": "لم أتمكن من التقاط الصورة.",
|
"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