@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.
- package/README.md +74 -7
- package/assets/icons/qr.png +0 -0
- package/assets/sounds/voice/search_prompt_en.mp3 +0 -0
- package/assets/sounds/voice/search_prompt_fr.mp3 +0 -0
- package/assets/sounds/voice/search_prompt_pt.mp3 +0 -0
- package/example/App.tsx +16 -1
- package/example/app.json +9 -1
- package/lib/module/core/assets.js +1 -0
- package/lib/module/core/assets.js.map +1 -1
- package/lib/module/core/di.js +8 -1
- package/lib/module/core/di.js.map +1 -1
- package/lib/module/core/pi_spi_custom_theme.js +63 -0
- package/lib/module/core/pi_spi_custom_theme.js.map +1 -0
- package/lib/module/core/pi_spi_sdk_config.js +15 -3
- package/lib/module/core/pi_spi_sdk_config.js.map +1 -1
- package/lib/module/core/router.js +3 -0
- package/lib/module/core/router.js.map +1 -1
- package/lib/module/core/routes.js +14 -0
- package/lib/module/core/routes.js.map +1 -1
- package/lib/module/core/storage.js +27 -29
- package/lib/module/core/storage.js.map +1 -1
- package/lib/module/core/theme.js +51 -0
- package/lib/module/core/theme.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/l10n/app_localizations.js.map +1 -1
- package/lib/module/l10n/app_localizations_en.js +24 -1
- package/lib/module/l10n/app_localizations_en.js.map +1 -1
- package/lib/module/l10n/app_localizations_fr.js +24 -1
- package/lib/module/l10n/app_localizations_fr.js.map +1 -1
- package/lib/module/l10n/app_localizations_pt.js +24 -1
- package/lib/module/l10n/app_localizations_pt.js.map +1 -1
- package/lib/module/modules/alias/presentation/pages/alias_page_create_options.js +3 -3
- package/lib/module/modules/alias/presentation/pages/alias_page_create_options.js.map +1 -1
- package/lib/module/modules/alias/presentation/pages/alias_page_otp.js.map +1 -1
- package/lib/module/modules/categorie/presentation/pages/categorie_edit_icon_btn.js +2 -2
- package/lib/module/modules/categorie/presentation/pages/categorie_edit_icon_btn.js.map +1 -1
- package/lib/module/modules/categorie/presentation/pages/categorie_select_widget.js +12 -10
- package/lib/module/modules/categorie/presentation/pages/categorie_select_widget.js.map +1 -1
- package/lib/module/modules/compte/presentation/pages/solde_widget_with_qr.js +14 -9
- package/lib/module/modules/compte/presentation/pages/solde_widget_with_qr.js.map +1 -1
- package/lib/module/modules/home/presentation/pages/home_page.js +41 -2
- package/lib/module/modules/home/presentation/pages/home_page.js.map +1 -1
- package/lib/module/modules/home/presentation/pages/home_tab_compte.js +1 -2
- package/lib/module/modules/home/presentation/pages/home_tab_compte.js.map +1 -1
- package/lib/module/modules/home/presentation/pages/home_tab_compte_more_menu.js +2 -2
- package/lib/module/modules/home/presentation/pages/home_tab_compte_more_menu.js.map +1 -1
- package/lib/module/modules/introduction/presentation/pages/introduction_bar.js +8 -1
- package/lib/module/modules/introduction/presentation/pages/introduction_bar.js.map +1 -1
- package/lib/module/modules/introduction/presentation/pages/introduction_page.js +29 -4
- package/lib/module/modules/introduction/presentation/pages/introduction_page.js.map +1 -1
- package/lib/module/modules/notification/presentation/bloc/notification_bloc.js +11 -4
- package/lib/module/modules/notification/presentation/bloc/notification_bloc.js.map +1 -1
- package/lib/module/modules/notification/presentation/bloc/notification_state.js +7 -1
- package/lib/module/modules/notification/presentation/bloc/notification_state.js.map +1 -1
- package/lib/module/modules/notification/presentation/pages/notification_page_liste.js +6 -2
- package/lib/module/modules/notification/presentation/pages/notification_page_liste.js.map +1 -1
- package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js +2 -2
- package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js.map +1 -1
- package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js +2 -2
- package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js.map +1 -1
- package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js +156 -25
- package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js.map +1 -1
- package/lib/module/modules/profile/presentation/pages/web/profile_web_page.js.map +1 -1
- package/lib/module/modules/qrcode/presentation/pages/qrcode_alias_page.js.map +1 -1
- package/lib/module/modules/qrcode/presentation/pages/qrcode_alias_page_share.js +2 -2
- package/lib/module/modules/qrcode/presentation/pages/qrcode_alias_page_share.js.map +1 -1
- package/lib/module/modules/search/domain/models/fonctionnalite.js +199 -0
- package/lib/module/modules/search/domain/models/fonctionnalite.js.map +1 -0
- package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js +119 -0
- package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js.map +1 -0
- package/lib/module/modules/search/presentation/pages/search_page.js +359 -0
- package/lib/module/modules/search/presentation/pages/search_page.js.map +1 -0
- package/lib/module/modules/search/presentation/pages/search_page_voice_button.js +335 -0
- package/lib/module/modules/search/presentation/pages/search_page_voice_button.js.map +1 -0
- package/lib/module/modules/subscription/presentation/pages/subscription_menu_widget.js +2 -2
- package/lib/module/modules/subscription/presentation/pages/subscription_menu_widget.js.map +1 -1
- package/lib/module/modules/transactions/domain/models/transaction_send/transaction_send_command.js +6 -2
- package/lib/module/modules/transactions/domain/models/transaction_send/transaction_send_command.js.map +1 -1
- package/lib/module/modules/transactions/presentation/bloc/transaction_search/transaction_search_bloc.js +13 -1
- package/lib/module/modules/transactions/presentation/bloc/transaction_search/transaction_search_bloc.js.map +1 -1
- package/lib/module/modules/transactions/presentation/bloc/transaction_search/transaction_search_state.js +7 -1
- package/lib/module/modules/transactions/presentation/bloc/transaction_search/transaction_search_state.js.map +1 -1
- package/lib/module/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.js +3 -1
- package/lib/module/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.js.map +1 -1
- package/lib/module/modules/transactions/presentation/pages/transaction_jour_libelle.js +15 -0
- package/lib/module/modules/transactions/presentation/pages/transaction_jour_libelle.js.map +1 -0
- package/lib/module/modules/transactions/presentation/pages/transaction_list_item_widget.js +2 -1
- package/lib/module/modules/transactions/presentation/pages/transaction_list_item_widget.js.map +1 -1
- package/lib/module/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.js +5 -3
- package/lib/module/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.js.map +1 -1
- package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.js +216 -137
- package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.js.map +1 -1
- package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.js +8 -3
- package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.js.map +1 -1
- package/lib/module/modules/transactions/presentation/pages/transaction_send/transaction_send_page_options.js +3 -3
- package/lib/module/modules/transactions/presentation/pages/transaction_send/transaction_send_page_options.js.map +1 -1
- package/lib/module/shared/widgets/amount_widget.js +4 -1
- package/lib/module/shared/widgets/amount_widget.js.map +1 -1
- package/lib/module/shared/widgets/input_amount.js.map +1 -1
- package/lib/module/shared/widgets/input_text.js +2 -0
- package/lib/module/shared/widgets/input_text.js.map +1 -1
- package/lib/module/shared/widgets/menu_actions_widget.js +10 -1
- package/lib/module/shared/widgets/menu_actions_widget.js.map +1 -1
- package/lib/module/shared/widgets/pagination_loader_widget.js +29 -0
- package/lib/module/shared/widgets/pagination_loader_widget.js.map +1 -0
- package/lib/typescript/src/core/assets.d.ts +1 -0
- package/lib/typescript/src/core/assets.d.ts.map +1 -1
- package/lib/typescript/src/core/di.d.ts.map +1 -1
- package/lib/typescript/src/core/pi_spi_custom_theme.d.ts +49 -0
- package/lib/typescript/src/core/pi_spi_custom_theme.d.ts.map +1 -0
- package/lib/typescript/src/core/pi_spi_sdk_config.d.ts +15 -2
- package/lib/typescript/src/core/pi_spi_sdk_config.d.ts.map +1 -1
- package/lib/typescript/src/core/router.d.ts +1 -0
- package/lib/typescript/src/core/router.d.ts.map +1 -1
- package/lib/typescript/src/core/routes.d.ts.map +1 -1
- package/lib/typescript/src/core/storage.d.ts +11 -3
- package/lib/typescript/src/core/storage.d.ts.map +1 -1
- package/lib/typescript/src/core/theme.d.ts +19 -0
- package/lib/typescript/src/core/theme.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/l10n/app_localizations.d.ts +46 -0
- package/lib/typescript/src/l10n/app_localizations.d.ts.map +1 -1
- package/lib/typescript/src/l10n/app_localizations_en.d.ts.map +1 -1
- package/lib/typescript/src/l10n/app_localizations_fr.d.ts.map +1 -1
- package/lib/typescript/src/l10n/app_localizations_pt.d.ts.map +1 -1
- package/lib/typescript/src/modules/categorie/presentation/pages/categorie_select_widget.d.ts.map +1 -1
- package/lib/typescript/src/modules/compte/presentation/pages/solde_widget_with_qr.d.ts.map +1 -1
- package/lib/typescript/src/modules/home/presentation/pages/home_page.d.ts.map +1 -1
- package/lib/typescript/src/modules/introduction/presentation/pages/introduction_bar.d.ts.map +1 -1
- package/lib/typescript/src/modules/introduction/presentation/pages/introduction_page.d.ts.map +1 -1
- package/lib/typescript/src/modules/notification/presentation/bloc/notification_bloc.d.ts.map +1 -1
- package/lib/typescript/src/modules/notification/presentation/bloc/notification_state.d.ts +5 -0
- package/lib/typescript/src/modules/notification/presentation/bloc/notification_state.d.ts.map +1 -1
- package/lib/typescript/src/modules/notification/presentation/pages/notification_page_liste.d.ts.map +1 -1
- package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page.d.ts.map +1 -1
- package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.d.ts.map +1 -1
- package/lib/typescript/src/modules/profile/presentation/pages/web/profile_web_page.d.ts.map +1 -1
- package/lib/typescript/src/modules/search/domain/models/fonctionnalite.d.ts +70 -0
- package/lib/typescript/src/modules/search/domain/models/fonctionnalite.d.ts.map +1 -0
- package/lib/typescript/src/modules/search/presentation/pages/search_fonctionnalites.d.ts +21 -0
- package/lib/typescript/src/modules/search/presentation/pages/search_fonctionnalites.d.ts.map +1 -0
- package/lib/typescript/src/modules/search/presentation/pages/search_page.d.ts +21 -0
- package/lib/typescript/src/modules/search/presentation/pages/search_page.d.ts.map +1 -0
- package/lib/typescript/src/modules/search/presentation/pages/search_page_voice_button.d.ts +29 -0
- package/lib/typescript/src/modules/search/presentation/pages/search_page_voice_button.d.ts.map +1 -0
- package/lib/typescript/src/modules/transactions/domain/models/transaction_send/transaction_send_command.d.ts +1 -1
- package/lib/typescript/src/modules/transactions/domain/models/transaction_send/transaction_send_command.d.ts.map +1 -1
- package/lib/typescript/src/modules/transactions/presentation/bloc/transaction_search/transaction_search_bloc.d.ts.map +1 -1
- package/lib/typescript/src/modules/transactions/presentation/bloc/transaction_search/transaction_search_state.d.ts +5 -0
- package/lib/typescript/src/modules/transactions/presentation/bloc/transaction_search/transaction_search_state.d.ts.map +1 -1
- package/lib/typescript/src/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.d.ts +2 -1
- package/lib/typescript/src/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.d.ts.map +1 -1
- package/lib/typescript/src/modules/transactions/presentation/pages/transaction_jour_libelle.d.ts +4 -0
- package/lib/typescript/src/modules/transactions/presentation/pages/transaction_jour_libelle.d.ts.map +1 -0
- package/lib/typescript/src/modules/transactions/presentation/pages/transaction_list_item_widget.d.ts +2 -1
- package/lib/typescript/src/modules/transactions/presentation/pages/transaction_list_item_widget.d.ts.map +1 -1
- package/lib/typescript/src/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.d.ts.map +1 -1
- package/lib/typescript/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.d.ts +5 -1
- package/lib/typescript/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.d.ts.map +1 -1
- package/lib/typescript/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.d.ts.map +1 -1
- package/lib/typescript/src/shared/widgets/amount_widget.d.ts +3 -1
- package/lib/typescript/src/shared/widgets/amount_widget.d.ts.map +1 -1
- package/lib/typescript/src/shared/widgets/input_text.d.ts +3 -3
- package/lib/typescript/src/shared/widgets/input_text.d.ts.map +1 -1
- package/lib/typescript/src/shared/widgets/menu_actions_widget.d.ts +5 -1
- package/lib/typescript/src/shared/widgets/menu_actions_widget.d.ts.map +1 -1
- package/lib/typescript/src/shared/widgets/pagination_loader_widget.d.ts +7 -0
- package/lib/typescript/src/shared/widgets/pagination_loader_widget.d.ts.map +1 -0
- package/package.json +49 -35
- package/src/core/assets.ts +1 -0
- package/src/core/di.ts +8 -1
- package/src/core/pi_spi_custom_theme.ts +66 -0
- package/src/core/pi_spi_sdk_config.ts +18 -3
- package/src/core/router.tsx +3 -0
- package/src/core/routes.tsx +15 -0
- package/src/core/storage.ts +30 -22
- package/src/core/theme.ts +53 -0
- package/src/index.ts +2 -1
- package/src/l10n/app_localizations.ts +46 -0
- package/src/l10n/app_localizations_en.ts +23 -0
- package/src/l10n/app_localizations_fr.ts +23 -0
- package/src/l10n/app_localizations_pt.ts +23 -0
- package/src/modules/alias/presentation/pages/alias_page_create_options.tsx +3 -3
- package/src/modules/alias/presentation/pages/alias_page_otp.tsx +2 -2
- package/src/modules/categorie/presentation/pages/categorie_edit_icon_btn.tsx +2 -2
- package/src/modules/categorie/presentation/pages/categorie_select_widget.tsx +13 -9
- package/src/modules/compte/presentation/pages/solde_widget_with_qr.tsx +10 -3
- package/src/modules/home/presentation/pages/home_page.tsx +31 -2
- package/src/modules/home/presentation/pages/home_tab_compte.tsx +1 -1
- package/src/modules/home/presentation/pages/home_tab_compte_more_menu.tsx +2 -2
- package/src/modules/introduction/presentation/pages/introduction_bar.tsx +12 -2
- package/src/modules/introduction/presentation/pages/introduction_page.tsx +20 -2
- package/src/modules/notification/presentation/bloc/notification_bloc.ts +12 -3
- package/src/modules/notification/presentation/bloc/notification_state.ts +11 -1
- package/src/modules/notification/presentation/pages/notification_page_liste.tsx +4 -1
- package/src/modules/profile/presentation/pages/parametre/profile_parametre_page.tsx +5 -2
- package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.tsx +2 -2
- package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.tsx +95 -15
- package/src/modules/profile/presentation/pages/web/profile_web_page.tsx +3 -2
- package/src/modules/qrcode/presentation/pages/qrcode_alias_page.tsx +2 -2
- package/src/modules/qrcode/presentation/pages/qrcode_alias_page_share.tsx +2 -2
- package/src/modules/search/domain/models/fonctionnalite.ts +217 -0
- package/src/modules/search/presentation/pages/search_fonctionnalites.ts +135 -0
- package/src/modules/search/presentation/pages/search_page.tsx +314 -0
- package/src/modules/search/presentation/pages/search_page_voice_button.tsx +283 -0
- package/src/modules/subscription/presentation/pages/subscription_menu_widget.tsx +2 -2
- package/src/modules/transactions/domain/models/transaction_send/transaction_send_command.ts +6 -2
- package/src/modules/transactions/presentation/bloc/transaction_search/transaction_search_bloc.ts +15 -1
- package/src/modules/transactions/presentation/bloc/transaction_search/transaction_search_state.ts +10 -1
- package/src/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.tsx +3 -0
- package/src/modules/transactions/presentation/pages/transaction_jour_libelle.ts +17 -0
- package/src/modules/transactions/presentation/pages/transaction_list_item_widget.tsx +3 -1
- package/src/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.tsx +7 -4
- package/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_filters.tsx +204 -101
- package/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.tsx +6 -2
- package/src/modules/transactions/presentation/pages/transaction_send/transaction_send_page_options.tsx +3 -3
- package/src/shared/widgets/amount_widget.tsx +5 -2
- package/src/shared/widgets/input_amount.tsx +2 -2
- package/src/shared/widgets/input_text.tsx +8 -3
- package/src/shared/widgets/menu_actions_widget.tsx +8 -2
- 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={
|
|
32
|
+
progressBarItemWidth={itemWidth}
|
|
27
33
|
passed={i < widgetPosition}
|
|
28
34
|
current={i === widgetPosition}
|
|
29
35
|
/>,
|
|
30
36
|
);
|
|
31
37
|
}
|
|
32
|
-
return
|
|
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
|
-
<
|
|
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
|
-
|
|
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([...
|
|
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:
|
|
48
|
+
subtitle:
|
|
49
|
+
params[ConfigKey.preferedTheme] === Themer.customThemeCode
|
|
50
|
+
? traductions.appSettingPageMenuThemeCustom
|
|
51
|
+
: Themes.label(theme, traductions),
|
|
49
52
|
}),
|
|
50
53
|
);
|
|
51
54
|
}
|
package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.tsx
CHANGED
|
@@ -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,
|
|
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
|
-
<
|
|
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
|
-
<
|
|
81
|
+
<ThemeOption
|
|
72
82
|
key={item}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
style={styles.
|
|
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
|
|
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
|
-
|
|
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<
|
|
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,
|
|
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
|
-
<
|
|
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
|
+
}
|