@pi-spi/app 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +74 -7
  2. package/assets/icons/qr.png +0 -0
  3. package/assets/sounds/voice/search_prompt_en.mp3 +0 -0
  4. package/assets/sounds/voice/search_prompt_fr.mp3 +0 -0
  5. package/assets/sounds/voice/search_prompt_pt.mp3 +0 -0
  6. package/example/App.tsx +16 -1
  7. package/example/app.json +9 -1
  8. package/lib/module/core/assets.js +1 -0
  9. package/lib/module/core/assets.js.map +1 -1
  10. package/lib/module/core/di.js +8 -1
  11. package/lib/module/core/di.js.map +1 -1
  12. package/lib/module/core/pi_spi_custom_theme.js +63 -0
  13. package/lib/module/core/pi_spi_custom_theme.js.map +1 -0
  14. package/lib/module/core/pi_spi_sdk_config.js +15 -3
  15. package/lib/module/core/pi_spi_sdk_config.js.map +1 -1
  16. package/lib/module/core/router.js +3 -0
  17. package/lib/module/core/router.js.map +1 -1
  18. package/lib/module/core/routes.js +14 -0
  19. package/lib/module/core/routes.js.map +1 -1
  20. package/lib/module/core/storage.js +27 -29
  21. package/lib/module/core/storage.js.map +1 -1
  22. package/lib/module/core/theme.js +51 -0
  23. package/lib/module/core/theme.js.map +1 -1
  24. package/lib/module/index.js +1 -0
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/l10n/app_localizations.js.map +1 -1
  27. package/lib/module/l10n/app_localizations_en.js +24 -1
  28. package/lib/module/l10n/app_localizations_en.js.map +1 -1
  29. package/lib/module/l10n/app_localizations_fr.js +24 -1
  30. package/lib/module/l10n/app_localizations_fr.js.map +1 -1
  31. package/lib/module/l10n/app_localizations_pt.js +24 -1
  32. package/lib/module/l10n/app_localizations_pt.js.map +1 -1
  33. package/lib/module/modules/alias/presentation/pages/alias_page_create_options.js +3 -3
  34. package/lib/module/modules/alias/presentation/pages/alias_page_create_options.js.map +1 -1
  35. package/lib/module/modules/alias/presentation/pages/alias_page_otp.js.map +1 -1
  36. package/lib/module/modules/categorie/presentation/pages/categorie_edit_icon_btn.js +2 -2
  37. package/lib/module/modules/categorie/presentation/pages/categorie_edit_icon_btn.js.map +1 -1
  38. package/lib/module/modules/categorie/presentation/pages/categorie_select_widget.js +12 -10
  39. package/lib/module/modules/categorie/presentation/pages/categorie_select_widget.js.map +1 -1
  40. package/lib/module/modules/compte/presentation/pages/solde_widget_with_qr.js +14 -9
  41. package/lib/module/modules/compte/presentation/pages/solde_widget_with_qr.js.map +1 -1
  42. package/lib/module/modules/home/presentation/pages/home_page.js +41 -2
  43. package/lib/module/modules/home/presentation/pages/home_page.js.map +1 -1
  44. package/lib/module/modules/home/presentation/pages/home_tab_compte.js +1 -2
  45. package/lib/module/modules/home/presentation/pages/home_tab_compte.js.map +1 -1
  46. package/lib/module/modules/home/presentation/pages/home_tab_compte_more_menu.js +2 -2
  47. package/lib/module/modules/home/presentation/pages/home_tab_compte_more_menu.js.map +1 -1
  48. package/lib/module/modules/introduction/presentation/pages/introduction_bar.js +8 -1
  49. package/lib/module/modules/introduction/presentation/pages/introduction_bar.js.map +1 -1
  50. package/lib/module/modules/introduction/presentation/pages/introduction_page.js +29 -4
  51. package/lib/module/modules/introduction/presentation/pages/introduction_page.js.map +1 -1
  52. package/lib/module/modules/notification/presentation/bloc/notification_bloc.js +11 -4
  53. package/lib/module/modules/notification/presentation/bloc/notification_bloc.js.map +1 -1
  54. package/lib/module/modules/notification/presentation/bloc/notification_state.js +7 -1
  55. package/lib/module/modules/notification/presentation/bloc/notification_state.js.map +1 -1
  56. package/lib/module/modules/notification/presentation/pages/notification_page_liste.js +6 -2
  57. package/lib/module/modules/notification/presentation/pages/notification_page_liste.js.map +1 -1
  58. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js +2 -2
  59. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js.map +1 -1
  60. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js +2 -2
  61. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js.map +1 -1
  62. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js +156 -25
  63. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js.map +1 -1
  64. package/lib/module/modules/profile/presentation/pages/web/profile_web_page.js.map +1 -1
  65. package/lib/module/modules/qrcode/presentation/pages/qrcode_alias_page.js.map +1 -1
  66. package/lib/module/modules/qrcode/presentation/pages/qrcode_alias_page_share.js +2 -2
  67. package/lib/module/modules/qrcode/presentation/pages/qrcode_alias_page_share.js.map +1 -1
  68. package/lib/module/modules/search/domain/models/fonctionnalite.js +199 -0
  69. package/lib/module/modules/search/domain/models/fonctionnalite.js.map +1 -0
  70. package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js +119 -0
  71. package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js.map +1 -0
  72. package/lib/module/modules/search/presentation/pages/search_page.js +359 -0
  73. package/lib/module/modules/search/presentation/pages/search_page.js.map +1 -0
  74. package/lib/module/modules/search/presentation/pages/search_page_voice_button.js +335 -0
  75. package/lib/module/modules/search/presentation/pages/search_page_voice_button.js.map +1 -0
  76. package/lib/module/modules/subscription/presentation/pages/subscription_menu_widget.js +2 -2
  77. package/lib/module/modules/subscription/presentation/pages/subscription_menu_widget.js.map +1 -1
  78. package/lib/module/modules/transactions/domain/models/transaction_send/transaction_send_command.js +6 -2
  79. package/lib/module/modules/transactions/domain/models/transaction_send/transaction_send_command.js.map +1 -1
  80. package/lib/module/modules/transactions/presentation/bloc/transaction_search/transaction_search_bloc.js +13 -1
  81. package/lib/module/modules/transactions/presentation/bloc/transaction_search/transaction_search_bloc.js.map +1 -1
  82. package/lib/module/modules/transactions/presentation/bloc/transaction_search/transaction_search_state.js +7 -1
  83. package/lib/module/modules/transactions/presentation/bloc/transaction_search/transaction_search_state.js.map +1 -1
  84. package/lib/module/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.js +3 -1
  85. package/lib/module/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.js.map +1 -1
  86. package/lib/module/modules/transactions/presentation/pages/transaction_jour_libelle.js +15 -0
  87. package/lib/module/modules/transactions/presentation/pages/transaction_jour_libelle.js.map +1 -0
  88. package/lib/module/modules/transactions/presentation/pages/transaction_list_item_widget.js +2 -1
  89. package/lib/module/modules/transactions/presentation/pages/transaction_list_item_widget.js.map +1 -1
  90. package/lib/module/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.js +5 -3
  91. package/lib/module/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.js.map +1 -1
  92. package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.js +216 -137
  93. package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.js.map +1 -1
  94. package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.js +8 -3
  95. package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.js.map +1 -1
  96. package/lib/module/modules/transactions/presentation/pages/transaction_send/transaction_send_page_options.js +3 -3
  97. package/lib/module/modules/transactions/presentation/pages/transaction_send/transaction_send_page_options.js.map +1 -1
  98. package/lib/module/shared/widgets/amount_widget.js +4 -1
  99. package/lib/module/shared/widgets/amount_widget.js.map +1 -1
  100. package/lib/module/shared/widgets/input_amount.js.map +1 -1
  101. package/lib/module/shared/widgets/input_text.js +2 -0
  102. package/lib/module/shared/widgets/input_text.js.map +1 -1
  103. package/lib/module/shared/widgets/menu_actions_widget.js +10 -1
  104. package/lib/module/shared/widgets/menu_actions_widget.js.map +1 -1
  105. package/lib/module/shared/widgets/pagination_loader_widget.js +29 -0
  106. package/lib/module/shared/widgets/pagination_loader_widget.js.map +1 -0
  107. package/lib/typescript/src/core/assets.d.ts +1 -0
  108. package/lib/typescript/src/core/assets.d.ts.map +1 -1
  109. package/lib/typescript/src/core/di.d.ts.map +1 -1
  110. package/lib/typescript/src/core/pi_spi_custom_theme.d.ts +49 -0
  111. package/lib/typescript/src/core/pi_spi_custom_theme.d.ts.map +1 -0
  112. package/lib/typescript/src/core/pi_spi_sdk_config.d.ts +15 -2
  113. package/lib/typescript/src/core/pi_spi_sdk_config.d.ts.map +1 -1
  114. package/lib/typescript/src/core/router.d.ts +1 -0
  115. package/lib/typescript/src/core/router.d.ts.map +1 -1
  116. package/lib/typescript/src/core/routes.d.ts.map +1 -1
  117. package/lib/typescript/src/core/storage.d.ts +11 -3
  118. package/lib/typescript/src/core/storage.d.ts.map +1 -1
  119. package/lib/typescript/src/core/theme.d.ts +19 -0
  120. package/lib/typescript/src/core/theme.d.ts.map +1 -1
  121. package/lib/typescript/src/index.d.ts +2 -1
  122. package/lib/typescript/src/index.d.ts.map +1 -1
  123. package/lib/typescript/src/l10n/app_localizations.d.ts +46 -0
  124. package/lib/typescript/src/l10n/app_localizations.d.ts.map +1 -1
  125. package/lib/typescript/src/l10n/app_localizations_en.d.ts.map +1 -1
  126. package/lib/typescript/src/l10n/app_localizations_fr.d.ts.map +1 -1
  127. package/lib/typescript/src/l10n/app_localizations_pt.d.ts.map +1 -1
  128. package/lib/typescript/src/modules/categorie/presentation/pages/categorie_select_widget.d.ts.map +1 -1
  129. package/lib/typescript/src/modules/compte/presentation/pages/solde_widget_with_qr.d.ts.map +1 -1
  130. package/lib/typescript/src/modules/home/presentation/pages/home_page.d.ts.map +1 -1
  131. package/lib/typescript/src/modules/introduction/presentation/pages/introduction_bar.d.ts.map +1 -1
  132. package/lib/typescript/src/modules/introduction/presentation/pages/introduction_page.d.ts.map +1 -1
  133. package/lib/typescript/src/modules/notification/presentation/bloc/notification_bloc.d.ts.map +1 -1
  134. package/lib/typescript/src/modules/notification/presentation/bloc/notification_state.d.ts +5 -0
  135. package/lib/typescript/src/modules/notification/presentation/bloc/notification_state.d.ts.map +1 -1
  136. package/lib/typescript/src/modules/notification/presentation/pages/notification_page_liste.d.ts.map +1 -1
  137. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page.d.ts.map +1 -1
  138. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.d.ts.map +1 -1
  139. package/lib/typescript/src/modules/profile/presentation/pages/web/profile_web_page.d.ts.map +1 -1
  140. package/lib/typescript/src/modules/search/domain/models/fonctionnalite.d.ts +70 -0
  141. package/lib/typescript/src/modules/search/domain/models/fonctionnalite.d.ts.map +1 -0
  142. package/lib/typescript/src/modules/search/presentation/pages/search_fonctionnalites.d.ts +21 -0
  143. package/lib/typescript/src/modules/search/presentation/pages/search_fonctionnalites.d.ts.map +1 -0
  144. package/lib/typescript/src/modules/search/presentation/pages/search_page.d.ts +21 -0
  145. package/lib/typescript/src/modules/search/presentation/pages/search_page.d.ts.map +1 -0
  146. package/lib/typescript/src/modules/search/presentation/pages/search_page_voice_button.d.ts +29 -0
  147. package/lib/typescript/src/modules/search/presentation/pages/search_page_voice_button.d.ts.map +1 -0
  148. package/lib/typescript/src/modules/transactions/domain/models/transaction_send/transaction_send_command.d.ts +1 -1
  149. package/lib/typescript/src/modules/transactions/domain/models/transaction_send/transaction_send_command.d.ts.map +1 -1
  150. package/lib/typescript/src/modules/transactions/presentation/bloc/transaction_search/transaction_search_bloc.d.ts.map +1 -1
  151. package/lib/typescript/src/modules/transactions/presentation/bloc/transaction_search/transaction_search_state.d.ts +5 -0
  152. package/lib/typescript/src/modules/transactions/presentation/bloc/transaction_search/transaction_search_state.d.ts.map +1 -1
  153. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.d.ts +2 -1
  154. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.d.ts.map +1 -1
  155. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_jour_libelle.d.ts +4 -0
  156. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_jour_libelle.d.ts.map +1 -0
  157. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_list_item_widget.d.ts +2 -1
  158. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_list_item_widget.d.ts.map +1 -1
  159. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.d.ts.map +1 -1
  160. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.d.ts +5 -1
  161. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.d.ts.map +1 -1
  162. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.d.ts.map +1 -1
  163. package/lib/typescript/src/shared/widgets/amount_widget.d.ts +3 -1
  164. package/lib/typescript/src/shared/widgets/amount_widget.d.ts.map +1 -1
  165. package/lib/typescript/src/shared/widgets/input_text.d.ts +3 -3
  166. package/lib/typescript/src/shared/widgets/input_text.d.ts.map +1 -1
  167. package/lib/typescript/src/shared/widgets/menu_actions_widget.d.ts +5 -1
  168. package/lib/typescript/src/shared/widgets/menu_actions_widget.d.ts.map +1 -1
  169. package/lib/typescript/src/shared/widgets/pagination_loader_widget.d.ts +7 -0
  170. package/lib/typescript/src/shared/widgets/pagination_loader_widget.d.ts.map +1 -0
  171. package/package.json +49 -35
  172. package/src/core/assets.ts +1 -0
  173. package/src/core/di.ts +8 -1
  174. package/src/core/pi_spi_custom_theme.ts +66 -0
  175. package/src/core/pi_spi_sdk_config.ts +18 -3
  176. package/src/core/router.tsx +3 -0
  177. package/src/core/routes.tsx +15 -0
  178. package/src/core/storage.ts +30 -22
  179. package/src/core/theme.ts +53 -0
  180. package/src/index.ts +2 -1
  181. package/src/l10n/app_localizations.ts +46 -0
  182. package/src/l10n/app_localizations_en.ts +23 -0
  183. package/src/l10n/app_localizations_fr.ts +23 -0
  184. package/src/l10n/app_localizations_pt.ts +23 -0
  185. package/src/modules/alias/presentation/pages/alias_page_create_options.tsx +3 -3
  186. package/src/modules/alias/presentation/pages/alias_page_otp.tsx +2 -2
  187. package/src/modules/categorie/presentation/pages/categorie_edit_icon_btn.tsx +2 -2
  188. package/src/modules/categorie/presentation/pages/categorie_select_widget.tsx +13 -9
  189. package/src/modules/compte/presentation/pages/solde_widget_with_qr.tsx +10 -3
  190. package/src/modules/home/presentation/pages/home_page.tsx +31 -2
  191. package/src/modules/home/presentation/pages/home_tab_compte.tsx +1 -1
  192. package/src/modules/home/presentation/pages/home_tab_compte_more_menu.tsx +2 -2
  193. package/src/modules/introduction/presentation/pages/introduction_bar.tsx +12 -2
  194. package/src/modules/introduction/presentation/pages/introduction_page.tsx +20 -2
  195. package/src/modules/notification/presentation/bloc/notification_bloc.ts +12 -3
  196. package/src/modules/notification/presentation/bloc/notification_state.ts +11 -1
  197. package/src/modules/notification/presentation/pages/notification_page_liste.tsx +4 -1
  198. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page.tsx +5 -2
  199. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.tsx +2 -2
  200. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.tsx +95 -15
  201. package/src/modules/profile/presentation/pages/web/profile_web_page.tsx +3 -2
  202. package/src/modules/qrcode/presentation/pages/qrcode_alias_page.tsx +2 -2
  203. package/src/modules/qrcode/presentation/pages/qrcode_alias_page_share.tsx +2 -2
  204. package/src/modules/search/domain/models/fonctionnalite.ts +217 -0
  205. package/src/modules/search/presentation/pages/search_fonctionnalites.ts +135 -0
  206. package/src/modules/search/presentation/pages/search_page.tsx +314 -0
  207. package/src/modules/search/presentation/pages/search_page_voice_button.tsx +283 -0
  208. package/src/modules/subscription/presentation/pages/subscription_menu_widget.tsx +2 -2
  209. package/src/modules/transactions/domain/models/transaction_send/transaction_send_command.ts +6 -2
  210. package/src/modules/transactions/presentation/bloc/transaction_search/transaction_search_bloc.ts +15 -1
  211. package/src/modules/transactions/presentation/bloc/transaction_search/transaction_search_state.ts +10 -1
  212. package/src/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.tsx +3 -0
  213. package/src/modules/transactions/presentation/pages/transaction_jour_libelle.ts +17 -0
  214. package/src/modules/transactions/presentation/pages/transaction_list_item_widget.tsx +3 -1
  215. package/src/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.tsx +7 -4
  216. package/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.tsx +204 -101
  217. package/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.tsx +6 -2
  218. package/src/modules/transactions/presentation/pages/transaction_send/transaction_send_page_options.tsx +3 -3
  219. package/src/shared/widgets/amount_widget.tsx +5 -2
  220. package/src/shared/widgets/input_amount.tsx +2 -2
  221. package/src/shared/widgets/input_text.tsx +8 -3
  222. package/src/shared/widgets/menu_actions_widget.tsx +8 -2
  223. package/src/shared/widgets/pagination_loader_widget.tsx +24 -0
@@ -1,4 +1,5 @@
1
1
  import type { VideoPlayer } from 'expo-video';
2
+ import { useState } from 'react';
2
3
  import { StyleSheet, View } from 'react-native';
3
4
 
4
5
  import type { IntroductionItem } from './introduction_item';
@@ -15,6 +16,11 @@ export function IntroductionBar({
15
16
  pageSize: number;
16
17
  progressBarWidth: number;
17
18
  }) {
19
+ // Largeur des segments tirée de la place réellement disponible, pas de
20
+ // [progressBarWidth] (calculé sur tout l'écran) : sur un écran déplié
21
+ // (Galaxy Z Fold), les marges système la réduisent et la barre débordait
22
+ const [largeur, setLargeur] = useState<number | null>(null);
23
+ const itemWidth = largeur != null ? largeur / pageSize - 4 : progressBarWidth;
18
24
  const widgetPosition = item.position;
19
25
  const barList = [];
20
26
  for (let i = 0; i < pageSize; i++) {
@@ -23,13 +29,17 @@ export function IntroductionBar({
23
29
  <VideoProgressBar
24
30
  key={i}
25
31
  videoPlayer={i === widgetPosition ? videoPlayer : null}
26
- progressBarItemWidth={progressBarWidth}
32
+ progressBarItemWidth={itemWidth}
27
33
  passed={i < widgetPosition}
28
34
  current={i === widgetPosition}
29
35
  />,
30
36
  );
31
37
  }
32
- return <View style={styles.row}>{barList}</View>;
38
+ return (
39
+ <View style={styles.row} onLayout={(event) => setLargeur(event.nativeEvent.layout.width)}>
40
+ {barList}
41
+ </View>
42
+ );
33
43
  }
34
44
 
35
45
  const styles = StyleSheet.create({
@@ -1,5 +1,5 @@
1
1
  import { useVideoPlayer } from 'expo-video';
2
- import { useEffect } from 'react';
2
+ import { useEffect, useState } from 'react';
3
3
  import { Pressable, StyleSheet, View } from 'react-native';
4
4
 
5
5
  import { useAppLocalizations } from '../../../../l10n/localizations';
@@ -28,6 +28,11 @@ export function IntroductionPageItem({
28
28
  onNext: () => void;
29
29
  }) {
30
30
  const traductions = useAppLocalizations();
31
+ // Vidéo et titres réduits si la hauteur manque (écran déplié, paysage),
32
+ // jamais agrandis : hauteur disponible / hauteur naturelle du bloc
33
+ const [hauteurZone, setHauteurZone] = useState(0);
34
+ const [hauteurContenu, setHauteurContenu] = useState(0);
35
+ const echelle = hauteurZone > 0 && hauteurContenu > hauteurZone ? hauteurZone / hauteurContenu : 1;
31
36
  const videoPlayer = useVideoPlayer(item.video, (player) => {
32
37
  player.loop = true;
33
38
  player.volume = 1.0;
@@ -59,7 +64,19 @@ export function IntroductionPageItem({
59
64
  {/* barre de progression */}
60
65
  <IntroductionBar videoPlayer={videoPlayer} item={item} pageSize={pageSize} progressBarWidth={progressBarWidth} />
61
66
  {/* Video et titres */}
62
- <IntroductionVideo videoPlayer={videoPlayer} item={item} />
67
+ <View
68
+ style={styles.zoneVideo}
69
+ pointerEvents="box-none"
70
+ onLayout={(event) => setHauteurZone(event.nativeEvent.layout.height)}
71
+ >
72
+ <View
73
+ style={{ transform: [{ scale: echelle }] }}
74
+ pointerEvents="box-none"
75
+ onLayout={(event) => setHauteurContenu(event.nativeEvent.layout.height)}
76
+ >
77
+ <IntroductionVideo videoPlayer={videoPlayer} item={item} />
78
+ </View>
79
+ </View>
63
80
  {/* Pied de la page */}
64
81
  <IntroductionFooter item={item} pageSize={pageSize} traductions={traductions} />
65
82
  </View>
@@ -72,4 +89,5 @@ const styles = StyleSheet.create({
72
89
  side: { flex: 1 },
73
90
  middle: { flex: 2 },
74
91
  page: { flex: 1, paddingVertical: 12, paddingHorizontal: 20, alignItems: 'stretch', justifyContent: 'space-between' },
92
+ zoneVideo: { flexShrink: 1, marginVertical: 12, alignItems: 'center', justifyContent: 'center' },
75
93
  });
@@ -147,8 +147,13 @@ export class NotificationBloc extends Bloc<NotificationEvent, NotificationState>
147
147
  private async onList(event: NotificationListEvent, emit: Emitter<NotificationState>): Promise<void> {
148
148
  // En cours de rechargement
149
149
  emit(new NotificationLoadingState(emptyListe(), this.state.count, event.command));
150
+
151
+ // Repartir de la première page
152
+ const command = event.command;
153
+ command.index = null;
154
+ command.total = 0;
155
+
150
156
  try {
151
- const command = event.command;
152
157
 
153
158
  // Rechercher d'abord en local et afficher
154
159
  const donneesLocales = await this.pNotificationInputPort.list(listParams(command));
@@ -186,7 +191,8 @@ export class NotificationBloc extends Bloc<NotificationEvent, NotificationState>
186
191
  if (this.paginating) return;
187
192
  this.paginating = true;
188
193
  const command = this.state.command;
189
- const oldListe = this.state.notifications;
194
+ // Affiche le loader en bas de la liste
195
+ emit(new NotificationInitialState(this.state.notifications, this.state.count, command, true));
190
196
  try {
191
197
  // Récupérer les notifications depuis le serveur
192
198
  const liste = await this.pNotificationInputPort.search(listParams(command, event.index));
@@ -196,16 +202,19 @@ export class NotificationBloc extends Bloc<NotificationEvent, NotificationState>
196
202
  if (newNotifications.length > 0) {
197
203
  emit(
198
204
  new NotificationInitialState(
199
- new NotificationListe([...oldListe.data, ...newNotifications], liste.meta),
205
+ new NotificationListe([...this.state.notifications.data, ...newNotifications], liste.meta),
200
206
  this.state.count,
201
207
  command,
202
208
  ),
203
209
  );
210
+ return;
204
211
  }
205
212
  } catch (e) {
206
213
  this.logger.e('Recuperer liste notification pagination error =>', { error: e });
207
214
  } finally {
208
215
  this.paginating = false;
209
216
  }
217
+ // Page vide ou erreur : on retire le loader
218
+ emit(new NotificationInitialState(this.state.notifications, this.state.count, command));
210
219
  }
211
220
  }
@@ -11,7 +11,17 @@ export abstract class NotificationState {
11
11
  ) {}
12
12
  }
13
13
 
14
- export class NotificationInitialState extends NotificationState {}
14
+ export class NotificationInitialState extends NotificationState {
15
+ constructor(
16
+ notifications: NotificationListe,
17
+ count: number,
18
+ command: NotificationSearchCommand,
19
+ /** true pendant le chargement de la page suivante : un petit loader est affiché en bas de la liste. */
20
+ readonly isPaginating = false,
21
+ ) {
22
+ super(notifications, count, command);
23
+ }
24
+ }
15
25
 
16
26
  export class NotificationLoadingState extends NotificationState {}
17
27
 
@@ -6,9 +6,10 @@ import { Themer, useTheme } from '../../../../core/theme';
6
6
  import { useAppLocalizations } from '../../../../l10n/localizations';
7
7
  import { Card } from '../../../../shared/material/card';
8
8
  import { DateFormat } from '../../../../shared/utils/date_format';
9
+ import { PaginationLoaderWidget } from '../../../../shared/widgets/pagination_loader_widget';
9
10
  import { NotificationBloc } from '../bloc/notification_bloc';
10
11
  import { NotificationListEvent, NotificationSearchPaginateEvent } from '../bloc/notification_event';
11
- import { NotificationLoadingState } from '../bloc/notification_state';
12
+ import { NotificationInitialState, NotificationLoadingState } from '../bloc/notification_state';
12
13
  import { groupNotifications } from './notification_groups';
13
14
  import { NotificationPageListeEmpty } from './notification_page_liste_empty';
14
15
  import { NotificationPageListeItem } from './notification_page_liste_item';
@@ -54,6 +55,8 @@ export function NotificationPageListe() {
54
55
  if (command.canLoadMore()) bloc.add(new NotificationSearchPaginateEvent(command.index));
55
56
  }}
56
57
  onEndReachedThreshold={0.2}
58
+ // Chargement de la page suivante
59
+ ListFooterComponent={state instanceof NotificationInitialState && state.isPaginating ? <PaginationLoaderWidget /> : undefined}
57
60
  renderItem={({ item }) => (
58
61
  <View>
59
62
  {/* Jour */}
@@ -5,7 +5,7 @@ import { useBloc, useBlocBuilder } from '../../../../../core/bloc_react';
5
5
  import { Di } from '../../../../../core/di';
6
6
  import { AppRouter, useAppRouter } from '../../../../../core/router';
7
7
  import { SoundManager } from '../../../../../core/sounds';
8
- import { Themes, useTheme } from '../../../../../core/theme';
8
+ import { Themer, Themes, useTheme } from '../../../../../core/theme';
9
9
  import type { AppLocalizations } from '../../../../../l10n/app_localizations';
10
10
  import { useAppLocalizations } from '../../../../../l10n/localizations';
11
11
  import { AppBar, Scaffold } from '../../../../../shared/material/scaffold';
@@ -45,7 +45,10 @@ function menuItems(traductions: AppLocalizations, configParams: ConfigParams): P
45
45
  if (Di.config.appConfig.canChangeTheme) {
46
46
  items.push(
47
47
  new ProfileItem(Images.iconsTheme, traductions.appSettingPageMenuThemeTitle, AppRouter.profileParametreTheme, {
48
- subtitle: Themes.label(theme, traductions),
48
+ subtitle:
49
+ params[ConfigKey.preferedTheme] === Themer.customThemeCode
50
+ ? traductions.appSettingPageMenuThemeCustom
51
+ : Themes.label(theme, traductions),
49
52
  }),
50
53
  );
51
54
  }
@@ -10,7 +10,7 @@ import { AppRouter, useAppRouter } from '../../../../../core/router';
10
10
  import { Themer, useTheme } from '../../../../../core/theme';
11
11
  import { useAppLocalizations } from '../../../../../l10n/localizations';
12
12
  import { Card } from '../../../../../shared/material/card';
13
- import { MenuActionItem, MenuActionsWiget } from '../../../../../shared/widgets/menu_actions_widget';
13
+ import { MenuActionItem, MenuActionsWidget } from '../../../../../shared/widgets/menu_actions_widget';
14
14
  import { ConfigBloc } from '../../../../config/adapters/ui/bloc/config_bloc';
15
15
  import { ConfigChangeEvent } from '../../../../config/adapters/ui/bloc/config_event';
16
16
  import { ConfigLoadedState } from '../../../../config/adapters/ui/bloc/config_state';
@@ -116,7 +116,7 @@ export function ProfileParametrePageNotifications() {
116
116
  <View style={styles.gap20} />
117
117
  <Text style={theme.textTheme.headlineMedium}>{traductions.appSettingPageMenuNotificationSonTitle}</Text>
118
118
  <View style={styles.gap10} />
119
- <MenuActionsWiget
119
+ <MenuActionsWidget
120
120
  items={[
121
121
  new MenuActionItem(
122
122
  Images.iconsCloche,
@@ -1,10 +1,11 @@
1
1
  import { Image } from 'expo-image';
2
+ import type { ReactNode } from 'react';
2
3
  import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
3
4
 
4
5
  import { Images } from '../../../../../core/assets';
5
6
  import { useBloc, useBlocBuilder } from '../../../../../core/bloc_react';
6
7
  import { AppRouter, useAppRouter } from '../../../../../core/router';
7
- import { Themes, useTheme } from '../../../../../core/theme';
8
+ import { Themer, Themes, useTheme, type ThemeData } from '../../../../../core/theme';
8
9
  import type { AppLocalizations } from '../../../../../l10n/app_localizations';
9
10
  import { useAppLocalizations } from '../../../../../l10n/localizations';
10
11
  import { Card } from '../../../../../shared/material/card';
@@ -67,21 +68,23 @@ export function ProfileParametrePageTheme() {
67
68
  <View style={styles.gap10} />
68
69
  <Card>
69
70
  <View style={styles.wrap}>
71
+ {/* Thème personnalisé de l'hôte, en tête */}
72
+ {Themer.customTheme != null && (
73
+ <ThemeOption
74
+ title={traductions.appSettingPageMenuThemeCustom}
75
+ selected={currentTheme === Themer.customThemeCode}
76
+ onSelect={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, Themer.customThemeCode))}
77
+ preview={<CustomThemePreview theme={Themer.customTheme} />}
78
+ />
79
+ )}
70
80
  {Themes.list().map((item) => (
71
- <Pressable
81
+ <ThemeOption
72
82
  key={item}
73
- accessibilityRole="button"
74
- disabled={currentTheme === item}
75
- onPress={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, item))}
76
- style={styles.item}
77
- >
78
- <Image source={themeIcon(item)} style={[styles.preview, { backgroundColor: theme.colorScheme.surface }]} contentFit="cover" />
79
- <View style={styles.gap5} />
80
- <Text style={theme.textTheme.displayMedium}>{themeTitle(traductions, item)}</Text>
81
- <View style={styles.gap5} />
82
- <CheckedDot value={currentTheme === item} />
83
- <View style={styles.gap20} />
84
- </Pressable>
83
+ title={themeTitle(traductions, item)}
84
+ selected={currentTheme === item}
85
+ onSelect={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, item))}
86
+ preview={<Image source={themeIcon(item)} style={styles.flex} contentFit="cover" />}
87
+ />
85
88
  ))}
86
89
  </View>
87
90
  </Card>
@@ -91,10 +94,87 @@ export function ProfileParametrePageTheme() {
91
94
  );
92
95
  }
93
96
 
97
+ /** Une option de thème : aperçu, nom et pastille de sélection */
98
+ function ThemeOption({
99
+ title,
100
+ selected,
101
+ onSelect,
102
+ preview,
103
+ }: {
104
+ title: string;
105
+ selected: boolean;
106
+ onSelect: () => void;
107
+ preview: ReactNode;
108
+ }) {
109
+ const theme = useTheme();
110
+ return (
111
+ <Pressable accessibilityRole="button" disabled={selected} onPress={onSelect} style={styles.item}>
112
+ <View style={[styles.preview, { backgroundColor: theme.colorScheme.surface }]}>{preview}</View>
113
+ <View style={styles.gap5} />
114
+ <Text style={theme.textTheme.displayMedium}>{title}</Text>
115
+ <View style={styles.gap5} />
116
+ <CheckedDot value={selected} />
117
+ <View style={styles.gap20} />
118
+ </Pressable>
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Aperçu du thème personnalisé de l'hôte (pas d'image comme pour les
124
+ * thèmes du SDK) : une miniature d'accueil dessinée avec ses couleurs.
125
+ */
126
+ function CustomThemePreview({ theme }: { theme: ThemeData }) {
127
+ const primary = theme.colorScheme.primary;
128
+ const secondary = theme.colorScheme.secondary;
129
+ const bar = (width: number, color: string) => <View style={[styles.bar, { width, backgroundColor: color }]} />;
130
+ return (
131
+ <View style={[styles.flex, styles.mock, { backgroundColor: theme.scaffoldBackgroundColor }]}>
132
+ {/* Avatar et titre */}
133
+ <View style={styles.mockRow}>
134
+ <View style={[styles.dot16, { backgroundColor: secondary }]} />
135
+ {bar(40, Themer.neural02Color)}
136
+ </View>
137
+ <View style={styles.gap12} />
138
+ {/* Carte du compte */}
139
+ <View style={[styles.mockCard, { backgroundColor: theme.cardColor }]}>
140
+ {bar(50, Themer.neural02Color)}
141
+ {bar(30, primary)}
142
+ </View>
143
+ <View style={styles.gap12} />
144
+ {/* Boutons d'action */}
145
+ <View style={styles.mockActions}>
146
+ {[0, 1, 2].map((i) => (
147
+ <View key={i} style={[styles.mockButton, { backgroundColor: primary }]} />
148
+ ))}
149
+ </View>
150
+ <View style={styles.gap14} />
151
+ {/* Liste */}
152
+ {[0, 1, 2].map((i) => (
153
+ <View key={i} style={[styles.mockRow, styles.mockListRow]}>
154
+ <View style={[styles.dot14, { backgroundColor: secondary }]} />
155
+ {bar(46, Themer.neural02Color)}
156
+ </View>
157
+ ))}
158
+ </View>
159
+ );
160
+ }
161
+
94
162
  const styles = StyleSheet.create({
163
+ flex: { flex: 1 },
164
+ mock: { padding: 10 },
165
+ mockRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
166
+ mockListRow: { marginBottom: 8 },
167
+ mockCard: { height: 54, padding: 8, borderRadius: 8, justifyContent: 'space-between' },
168
+ mockActions: { flexDirection: 'row', justifyContent: 'space-between' },
169
+ mockButton: { width: 26, height: 26, borderRadius: 8 },
170
+ bar: { height: 6, borderRadius: 3 },
171
+ dot16: { width: 16, height: 16, borderRadius: 8 },
172
+ dot14: { width: 14, height: 14, borderRadius: 7 },
173
+ gap12: { height: 12 },
174
+ gap14: { height: 14 },
95
175
  wrap: { padding: 20, flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', columnGap: 10, rowGap: 10 },
96
176
  item: { alignItems: 'center' },
97
- preview: { width: 120, height: 200, borderRadius: 8 },
177
+ preview: { width: 120, height: 200, borderRadius: 8, overflow: 'hidden' },
98
178
  gap5: { height: 5 },
99
179
  gap10: { height: 10 },
100
180
  gap20: { height: 20 },
@@ -23,7 +23,7 @@ export function ProfileWebPage({ url }: { url: string }) {
23
23
  const theme = useTheme();
24
24
  const router = useAppRouter();
25
25
  const insets = useSafeAreaInsets();
26
- const webview = useRef<WebView>(null);
26
+ const webview = useRef<WebView<object>>(null);
27
27
  const [loading, setLoading] = useState(true);
28
28
  const [hasError, setHasError] = useState(false);
29
29
 
@@ -47,7 +47,8 @@ export function ProfileWebPage({ url }: { url: string }) {
47
47
  />
48
48
  ) : null}
49
49
  <View style={hasError ? styles.hidden : styles.flex}>
50
- <WebView
50
+ {/* react-native-webview 14 : sans paramètre de type, ses props valent `never` */}
51
+ <WebView<object>
51
52
  ref={webview}
52
53
  source={{ uri: url }}
53
54
  style={styles.web}
@@ -2,7 +2,7 @@ import { MaterialIcons } from '@expo/vector-icons';
2
2
  import { File, Paths } from 'expo-file-system';
3
3
  import * as Sharing from 'expo-sharing';
4
4
  import { useRef, useState } from 'react';
5
- import { ActivityIndicator, Platform, Pressable, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native';
5
+ import { ActivityIndicator, Platform, Pressable, ScrollView, StyleSheet, useWindowDimensions, View, type ViewInstance } from 'react-native';
6
6
  import { captureRef } from 'react-native-view-shot';
7
7
 
8
8
  import { useBloc, useBlocListener, useCreateBloc } from '../../../../core/bloc_react';
@@ -42,7 +42,7 @@ export function QrCodeAliasPage() {
42
42
  const aliasCle = alias?.getCle() ?? '';
43
43
  const [qrCode, setQrCode] = useState<string | null>(null);
44
44
  const [showQrcodeName, setShowQrcodeName] = useState(false);
45
- const qrRef = useRef<View>(null);
45
+ const qrRef = useRef<ViewInstance>(null);
46
46
  const bloc = useCreateBloc(() => {
47
47
  const created = new QrcodeAliasBloc(Di.getQrcodeInputPort());
48
48
  if (alias != null) created.add(new QrcodeAliasEncodeEvent(alias));
@@ -5,7 +5,7 @@ import { Images } from '../../../../core/assets';
5
5
  import { AppRouter, useAppRouter } from '../../../../core/router';
6
6
  import { useTheme } from '../../../../core/theme';
7
7
  import { useAppLocalizations } from '../../../../l10n/localizations';
8
- import { MenuActionItem, MenuActionsWiget } from '../../../../shared/widgets/menu_actions_widget';
8
+ import { MenuActionItem, MenuActionsWidget } from '../../../../shared/widgets/menu_actions_widget';
9
9
 
10
10
  /** Feuille « Partager » : le QR code (image) ou l'alias (texte). */
11
11
  export function QrcodeAliasPageShare({
@@ -28,7 +28,7 @@ export function QrcodeAliasPageShare({
28
28
  <ScrollView contentContainerStyle={[styles.content, { paddingBottom: 20 + insets.bottom }]}>
29
29
  <Text style={theme.textTheme.headlineMedium}>{traductions.qrcodePagePartageTitle}</Text>
30
30
  <View style={styles.gap20} />
31
- <MenuActionsWiget
31
+ <MenuActionsWidget
32
32
  items={[
33
33
  new MenuActionItem(
34
34
  Images.homeFooterQrcode,
@@ -0,0 +1,217 @@
1
+ /** Lettres accentuées et ligatures ramenées à leur forme simple. */
2
+ const ACCENTS: Record<string, string> = {
3
+ à: 'a',
4
+ â: 'a',
5
+ ä: 'a',
6
+ á: 'a',
7
+ ã: 'a',
8
+ å: 'a',
9
+ ç: 'c',
10
+ é: 'e',
11
+ è: 'e',
12
+ ê: 'e',
13
+ ë: 'e',
14
+ î: 'i',
15
+ ï: 'i',
16
+ í: 'i',
17
+ ì: 'i',
18
+ ô: 'o',
19
+ ö: 'o',
20
+ ó: 'o',
21
+ ò: 'o',
22
+ õ: 'o',
23
+ ù: 'u',
24
+ û: 'u',
25
+ ü: 'u',
26
+ ú: 'u',
27
+ ÿ: 'y',
28
+ ñ: 'n',
29
+ œ: 'oe',
30
+ æ: 'ae',
31
+ };
32
+
33
+ /**
34
+ * Une fonctionnalité de l'application que l'utilisateur peut retrouver
35
+ * depuis la recherche.
36
+ *
37
+ * La recherche ignore les majuscules, les accents et la ponctuation :
38
+ * « Sécurité », « securite » et « SECURITÉ » donnent le même résultat.
39
+ */
40
+ export class Fonctionnalite {
41
+ /** Icône (module Metro, voir `Images`). */
42
+ readonly icon: number;
43
+
44
+ /** Libellé affiché, déjà traduit. C'est le champ le plus pertinent. */
45
+ readonly titre: string;
46
+
47
+ /** Courte description affichée sous le titre, déjà traduite. */
48
+ readonly sousTitre?: string;
49
+
50
+ /**
51
+ * Synonymes non affichés, dans toutes les langues de l'application.
52
+ * Ils doivent être écrits sans accents (voir {@link Fonctionnalite.normaliser}).
53
+ */
54
+ readonly motsCles: readonly string[];
55
+
56
+ /**
57
+ * Ouvre la fonctionnalité. Appelé par la page appelante une fois la
58
+ * recherche fermée, jamais par la page de recherche elle-même.
59
+ */
60
+ readonly ouvrir: () => void;
61
+
62
+ constructor(options: {
63
+ icon: number;
64
+ titre: string;
65
+ sousTitre?: string;
66
+ motsCles?: readonly string[];
67
+ ouvrir: () => void;
68
+ }) {
69
+ this.icon = options.icon;
70
+ this.titre = options.titre;
71
+ this.sousTitre = options.sousTitre;
72
+ this.motsCles = options.motsCles ?? [];
73
+ this.ouvrir = options.ouvrir;
74
+ }
75
+
76
+ /**
77
+ * Met [texte] en minuscules sans accents, et remplace tout ce qui
78
+ * n'est ni lettre ni chiffre par un seul espace.
79
+ * Exemple : « Code QR — Scanner ! » devient « code qr scanner ».
80
+ */
81
+ static normaliser(texte: string): string {
82
+ let resultat = '';
83
+ for (const lettre of texte.toLowerCase()) {
84
+ resultat += ACCENTS[lettre] ?? lettre;
85
+ }
86
+ return resultat.replace(/[^a-z0-9]+/g, ' ').trim();
87
+ }
88
+
89
+ /** Mots normalisés de [texte], sans les vides. */
90
+ private static mots(texte: string): string[] {
91
+ return Fonctionnalite.normaliser(texte)
92
+ .split(' ')
93
+ .filter((mot) => mot.length > 0);
94
+ }
95
+
96
+ /**
97
+ * Mots utiles de la saisie. Si elle ne contient que des mots courants
98
+ * (« de » en cours de frappe de « demander »), ils sont tous gardés.
99
+ */
100
+ private static motsUtiles(recherche: string): string[] {
101
+ const mots = Fonctionnalite.mots(recherche);
102
+ const utiles = mots.filter((mot) => !MOTS_VIDES.has(mot));
103
+ return utiles.length === 0 ? mots : utiles;
104
+ }
105
+
106
+ /**
107
+ * Qualité de la correspondance entre un mot [saisi] et un [mot] de la
108
+ * fonctionnalité, 0 s'ils ne correspondent pas :
109
+ * - 3 : mots identiques ;
110
+ * - 2 : [mot] commence par [saisi] (frappe en cours : « envo » trouve
111
+ * « envoi »), ou l'inverse pour un [mot] d'au moins 4 lettres
112
+ * (pluriel : « abonnements » trouve « abonnement ») ;
113
+ * - 1 : une seule faute de frappe, sur des mots d'au moins 5 lettres
114
+ * (« securiter », « abonement »).
115
+ */
116
+ private static qualite(saisi: string, mot: string): number {
117
+ if (saisi === mot) return 3;
118
+ if (mot.startsWith(saisi)) return 2;
119
+ if (mot.length >= 4 && saisi.startsWith(mot)) return 2;
120
+ if (saisi.length >= 5 && mot.length >= 5 && uneFaute(saisi, mot)) return 1;
121
+ return 0;
122
+ }
123
+
124
+ /**
125
+ * Nombre de mots de la saisie trouvés dans la fonctionnalité, et points
126
+ * cumulés (qualité de la correspondance × poids du champ, la meilleure
127
+ * pour chaque mot saisi).
128
+ */
129
+ private score(saisis: readonly string[]): { trouves: number; points: number } {
130
+ const champs: [string[], number][] = [
131
+ [Fonctionnalite.mots(this.titre), POIDS_TITRE],
132
+ [this.motsCles.flatMap((motCle) => Fonctionnalite.mots(motCle)), POIDS_MOTS_CLES],
133
+ [Fonctionnalite.mots(this.sousTitre ?? ''), POIDS_SOUS_TITRE],
134
+ ];
135
+ let trouves = 0;
136
+ let points = 0;
137
+ for (const saisi of saisis) {
138
+ let meilleur = 0;
139
+ for (const [mots, poids] of champs) {
140
+ for (const mot of mots) {
141
+ meilleur = Math.max(meilleur, Fonctionnalite.qualite(saisi, mot) * poids);
142
+ }
143
+ }
144
+ if (meilleur > 0) {
145
+ trouves++;
146
+ points += meilleur;
147
+ }
148
+ }
149
+ return { trouves, points };
150
+ }
151
+
152
+ /**
153
+ * Fonctionnalités de [catalogue] dont au moins un mot utile de
154
+ * [recherche] est trouvé, triées par nombre de mots trouvés puis par
155
+ * points. À égalité, l'ordre du catalogue est conservé.
156
+ * Une saisie vide renvoie tout le catalogue.
157
+ */
158
+ static rechercher(catalogue: readonly Fonctionnalite[], recherche: string): Fonctionnalite[] {
159
+ const saisis = Fonctionnalite.motsUtiles(recherche);
160
+ if (saisis.length === 0) return [...catalogue];
161
+ return catalogue
162
+ .map((item, ordre) => ({ item, ordre, ...item.score(saisis) }))
163
+ .filter(({ trouves }) => trouves > 0)
164
+ .sort((a, b) => b.trouves - a.trouves || b.points - a.points || a.ordre - b.ordre)
165
+ .map(({ item }) => item);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Mots trop courants pour désigner une fonctionnalité, en français,
171
+ * anglais et portugais, sans accents. Ils sont ignorés dans la saisie,
172
+ * pour qu'une phrase dictée (« je veux envoyer de l'argent ») ne
173
+ * retienne que les mots utiles.
174
+ */
175
+ const MOTS_VIDES: ReadonlySet<string> = new Set([
176
+ // Français
177
+ 'je', 'j', 'tu', 'il', 'elle', 'on', 'nous', 'vous', 'me', 'm', 'te',
178
+ 't', 'se', 's', 'le', 'la', 'les', 'l', 'un', 'une', 'des', 'du', 'de',
179
+ 'd', 'au', 'aux', 'a', 'et', 'ou', 'en', 'y', 'pour', 'par', 'sur',
180
+ 'avec', 'dans', 'mon', 'ma', 'mes', 'ton', 'ta', 'tes', 'son', 'sa',
181
+ 'ses', 'notre', 'nos', 'votre', 'vos', 'ce', 'cet', 'cette', 'ces', 'c',
182
+ 'qu', 'que', 'qui', 'quoi', 'comment', 'est', 'veux', 'voudrais',
183
+ 'souhaite', 'aimerais', 'faire', 'peux', 'puis', 'voir', 'ne', 'n',
184
+ 'pas', 'svp', 'stp',
185
+ // Anglais
186
+ 'i', 'my', 'the', 'an', 'to', 'of', 'for', 'in', 'at', 'and', 'or',
187
+ 'is', 'want', 'would', 'like', 'how', 'can', 'do', 'please', 'make',
188
+ // Portugais
189
+ 'eu', 'o', 'os', 'as', 'um', 'uma', 'da', 'dos', 'das', 'no',
190
+ 'na', 'em', 'para', 'por', 'com', 'meu', 'minha', 'meus', 'minhas',
191
+ 'quero', 'queria', 'gostaria', 'como', 'fazer', 'ver', 'e',
192
+ ]);
193
+
194
+ /** Poids d'un mot trouvé selon le champ où il l'est. */
195
+ const POIDS_TITRE = 3;
196
+ const POIDS_MOTS_CLES = 2;
197
+ const POIDS_SOUS_TITRE = 1;
198
+
199
+ /** [a] et [b] ne diffèrent que d'une lettre remplacée, ajoutée ou retirée. */
200
+ function uneFaute(a: string, b: string): boolean {
201
+ if (Math.abs(a.length - b.length) > 1) return false;
202
+ const [court, long] = a.length <= b.length ? [a, b] : [b, a];
203
+ let i = 0;
204
+ let j = 0;
205
+ let fautes = 0;
206
+ while (i < court.length && j < long.length) {
207
+ if (court[i] === long[j]) {
208
+ i++;
209
+ j++;
210
+ continue;
211
+ }
212
+ if (++fautes > 1) return false;
213
+ if (court.length === long.length) i++;
214
+ j++;
215
+ }
216
+ return fautes + (long.length - j) <= 1;
217
+ }