react-native-maplibre-lite 0.2.5 → 0.2.6
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 +32 -12
- package/lib/module/components/MapView.js +19 -22
- package/lib/module/components/MapView.js.map +1 -1
- package/lib/module/components/NavigatorVoiceControl.js +9 -10
- package/lib/module/components/NavigatorVoiceControl.js.map +1 -1
- package/lib/module/components/navigatorVoicePlayer.js +2 -4
- package/lib/module/components/navigatorVoicePlayer.js.map +1 -1
- package/lib/module/components/types.js +6 -2
- package/lib/module/components/types.js.map +1 -1
- package/lib/module/components/useNavigatorTts.js +154 -0
- package/lib/module/components/useNavigatorTts.js.map +1 -0
- package/lib/module/components/webMapBuild.js +1 -1
- package/lib/module/components/webMapBuild.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/MapView.d.ts +8 -5
- package/lib/typescript/src/components/MapView.d.ts.map +1 -1
- package/lib/typescript/src/components/NavigatorVoiceControl.d.ts +6 -7
- package/lib/typescript/src/components/NavigatorVoiceControl.d.ts.map +1 -1
- package/lib/typescript/src/components/navigatorVoicePlayer.d.ts.map +1 -1
- package/lib/typescript/src/components/types.d.ts +8 -4
- package/lib/typescript/src/components/types.d.ts.map +1 -1
- package/lib/typescript/src/components/useNavigatorTts.d.ts +28 -0
- package/lib/typescript/src/components/useNavigatorTts.d.ts.map +1 -0
- package/lib/typescript/src/components/webMapBuild.d.ts +1 -1
- package/lib/typescript/src/components/webMapBuild.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/resources/map.html +42 -43
- package/src/components/MapView.tsx +34 -25
- package/src/components/NavigatorVoiceControl.tsx +13 -14
- package/src/components/navigatorVoicePlayer.tsx +2 -4
- package/src/components/types.ts +13 -4
- package/src/components/useNavigatorTts.ts +195 -0
- package/src/components/webMapBuild.ts +1 -1
- package/src/index.tsx +2 -0
- package/lib/module/components/navigatorVoiceCatalog.js +0 -261
- package/lib/module/components/navigatorVoiceCatalog.js.map +0 -1
- package/lib/module/components/navigatorVoiceKeys.js +0 -14
- package/lib/module/components/navigatorVoiceKeys.js.map +0 -1
- package/lib/module/components/useNavigatorVoice.js +0 -78
- package/lib/module/components/useNavigatorVoice.js.map +0 -1
- package/lib/typescript/src/components/navigatorVoiceCatalog.d.ts +0 -50
- package/lib/typescript/src/components/navigatorVoiceCatalog.d.ts.map +0 -1
- package/lib/typescript/src/components/navigatorVoiceKeys.d.ts +0 -10
- package/lib/typescript/src/components/navigatorVoiceKeys.d.ts.map +0 -1
- package/lib/typescript/src/components/useNavigatorVoice.d.ts +0 -20
- package/lib/typescript/src/components/useNavigatorVoice.d.ts.map +0 -1
- package/src/components/navigatorVoiceCatalog.ts +0 -316
- package/src/components/navigatorVoiceKeys.ts +0 -132
- package/src/components/useNavigatorVoice.ts +0 -96
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
-
|
|
3
|
-
import { isKnownPhraseKey, type VoicePhraseKey } from './navigatorVoiceKeys';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Каталог/манифест голосов навигатора и персист настроек. Порт
|
|
7
|
-
* `webProject/src/map/navigationVoiceCatalog.ts` на нативную часть:
|
|
8
|
-
* localStorage → AsyncStorage, плюс маппинг ключей фраз в URL клипов.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export type VoiceCatalogEntry = {
|
|
12
|
-
dir: string;
|
|
13
|
-
name: string;
|
|
14
|
-
locale: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/** Per-voice manifest from S3: phrase key → variant ids. */
|
|
18
|
-
export type VoiceManifest = Record<string, string[]>;
|
|
19
|
-
|
|
20
|
-
export type VoicePref = { dir: string } | { disabled: true };
|
|
21
|
-
|
|
22
|
-
export type VoiceRuntime = {
|
|
23
|
-
catalogUrl: string;
|
|
24
|
-
catalog: VoiceCatalogEntry[];
|
|
25
|
-
selectedDir: string;
|
|
26
|
-
clipBaseUrl: string;
|
|
27
|
-
manifest: VoiceManifest | null;
|
|
28
|
-
enabled: boolean;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const LS_VOICE_PREF = 'maplite-navigator-voice-pref';
|
|
32
|
-
const LS_VOICE_VOLUME = 'maplite-navigator-voice-volume';
|
|
33
|
-
|
|
34
|
-
export type VoiceVolumeLevel = 1 | 2 | 3 | 4 | 5;
|
|
35
|
-
|
|
36
|
-
const DEFAULT_VOICE_VOLUME_LEVEL: VoiceVolumeLevel = 5;
|
|
37
|
-
|
|
38
|
-
export function volumeLevelToGain(level: VoiceVolumeLevel): number {
|
|
39
|
-
return level * 0.2;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function readVoiceVolumeLevel(): Promise<VoiceVolumeLevel> {
|
|
43
|
-
try {
|
|
44
|
-
const raw = await AsyncStorage.getItem(LS_VOICE_VOLUME);
|
|
45
|
-
if (!raw) return DEFAULT_VOICE_VOLUME_LEVEL;
|
|
46
|
-
const n = Number.parseInt(raw, 10);
|
|
47
|
-
if (n >= 1 && n <= 5) return n as VoiceVolumeLevel;
|
|
48
|
-
} catch {
|
|
49
|
-
/* ignore */
|
|
50
|
-
}
|
|
51
|
-
return DEFAULT_VOICE_VOLUME_LEVEL;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function writeVoiceVolumeLevel(level: VoiceVolumeLevel): Promise<void> {
|
|
55
|
-
try {
|
|
56
|
-
await AsyncStorage.setItem(LS_VOICE_VOLUME, String(level));
|
|
57
|
-
} catch {
|
|
58
|
-
/* ignore */
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function readVoicePref(): Promise<VoicePref | null> {
|
|
63
|
-
try {
|
|
64
|
-
const raw = await AsyncStorage.getItem(LS_VOICE_PREF);
|
|
65
|
-
if (!raw) return null;
|
|
66
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
67
|
-
if (
|
|
68
|
-
parsed &&
|
|
69
|
-
typeof parsed === 'object' &&
|
|
70
|
-
'disabled' in parsed &&
|
|
71
|
-
(parsed as { disabled: unknown }).disabled === true
|
|
72
|
-
) {
|
|
73
|
-
return { disabled: true };
|
|
74
|
-
}
|
|
75
|
-
if (
|
|
76
|
-
parsed &&
|
|
77
|
-
typeof parsed === 'object' &&
|
|
78
|
-
'dir' in parsed &&
|
|
79
|
-
typeof (parsed as { dir: unknown }).dir === 'string'
|
|
80
|
-
) {
|
|
81
|
-
const dir = (parsed as { dir: string }).dir.trim();
|
|
82
|
-
if (dir) return { dir };
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
/* ignore */
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function writeVoicePref(pref: VoicePref): Promise<void> {
|
|
91
|
-
try {
|
|
92
|
-
await AsyncStorage.setItem(LS_VOICE_PREF, JSON.stringify(pref));
|
|
93
|
-
} catch {
|
|
94
|
-
/* ignore */
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const HTTP_URL_PREFIX_RE = /^https?:\/\//i;
|
|
99
|
-
|
|
100
|
-
function trimTrailingSlashes(s: string): string {
|
|
101
|
-
return s.replace(/\/+$/, '');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Strips `?query` and `#hash` — catalog URLs are plain paths. */
|
|
105
|
-
function stripUrlQueryAndHash(url: string): string {
|
|
106
|
-
const q = url.indexOf('?');
|
|
107
|
-
const h = url.indexOf('#');
|
|
108
|
-
let end = url.length;
|
|
109
|
-
if (q >= 0) end = Math.min(end, q);
|
|
110
|
-
if (h >= 0) end = Math.min(end, h);
|
|
111
|
-
return url.slice(0, end);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function pathnameSegments(pathname: string): string[] {
|
|
115
|
-
return pathname.split('/').filter(Boolean);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Parses `http(s)://host[:port]/path` without the URL API (Hermes-safe).
|
|
120
|
-
*/
|
|
121
|
-
function parseHttpUrl(url: string): { origin: string; pathname: string } | null {
|
|
122
|
-
const bare = stripUrlQueryAndHash(url.trim());
|
|
123
|
-
if (!HTTP_URL_PREFIX_RE.test(bare)) return null;
|
|
124
|
-
|
|
125
|
-
const schemeEnd = bare.indexOf('://');
|
|
126
|
-
const rest = bare.slice(schemeEnd + 3);
|
|
127
|
-
const slash = rest.indexOf('/');
|
|
128
|
-
const authority = slash === -1 ? rest : rest.slice(0, slash);
|
|
129
|
-
if (!authority) return null;
|
|
130
|
-
|
|
131
|
-
const pathname = slash === -1 ? '/' : rest.slice(slash);
|
|
132
|
-
const origin = bare.slice(0, schemeEnd + 3) + authority;
|
|
133
|
-
return { origin, pathname };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function resolveVoiceCatalogUrl(url: string): string | null {
|
|
137
|
-
const t = url.trim();
|
|
138
|
-
if (!t) return null;
|
|
139
|
-
return parseHttpUrl(t) ? t : null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Base URL for a voice directory: `.../voices/{dir}`. */
|
|
143
|
-
export function voiceDirBaseUrl(catalogUrl: string, dir: string): string {
|
|
144
|
-
const parsed = parseHttpUrl(catalogUrl);
|
|
145
|
-
if (!parsed) {
|
|
146
|
-
throw new Error('voiceDirBaseUrl: invalid catalog URL');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const parts = pathnameSegments(parsed.pathname);
|
|
150
|
-
if (parts.length > 0 && parts[parts.length - 1]!.toLowerCase().endsWith('.json')) {
|
|
151
|
-
parts.pop();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const safeDir = dir.trim().replace(/[/\\]+/g, '');
|
|
155
|
-
if (!safeDir) {
|
|
156
|
-
throw new Error('voiceDirBaseUrl: empty dir');
|
|
157
|
-
}
|
|
158
|
-
parts.push(safeDir);
|
|
159
|
-
|
|
160
|
-
return trimTrailingSlashes(`${parsed.origin}/${parts.join('/')}`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function voiceManifestUrl(baseUrl: string): string {
|
|
164
|
-
return `${trimTrailingSlashes(baseUrl)}/data.json`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export async function fetchVoiceCatalog(catalogUrl: string): Promise<VoiceCatalogEntry[]> {
|
|
168
|
-
const res = await fetch(catalogUrl);
|
|
169
|
-
if (!res.ok) {
|
|
170
|
-
throw new Error(`voice catalog fetch failed: ${res.status}`);
|
|
171
|
-
}
|
|
172
|
-
const data = (await res.json()) as unknown;
|
|
173
|
-
if (!Array.isArray(data)) {
|
|
174
|
-
throw new Error('voice catalog: expected JSON array');
|
|
175
|
-
}
|
|
176
|
-
const entries: VoiceCatalogEntry[] = [];
|
|
177
|
-
for (const item of data) {
|
|
178
|
-
if (
|
|
179
|
-
item &&
|
|
180
|
-
typeof item === 'object' &&
|
|
181
|
-
typeof (item as VoiceCatalogEntry).dir === 'string' &&
|
|
182
|
-
typeof (item as VoiceCatalogEntry).name === 'string'
|
|
183
|
-
) {
|
|
184
|
-
const e = item as VoiceCatalogEntry;
|
|
185
|
-
entries.push({
|
|
186
|
-
dir: e.dir.trim(),
|
|
187
|
-
name: e.name,
|
|
188
|
-
locale: typeof e.locale === 'string' ? e.locale : 'RU',
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (entries.length === 0) {
|
|
193
|
-
throw new Error('voice catalog: empty');
|
|
194
|
-
}
|
|
195
|
-
return entries;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export async function fetchVoiceManifest(clipBaseUrl: string): Promise<VoiceManifest> {
|
|
199
|
-
const res = await fetch(voiceManifestUrl(clipBaseUrl));
|
|
200
|
-
if (!res.ok) {
|
|
201
|
-
throw new Error(`voice manifest fetch failed: ${res.status}`);
|
|
202
|
-
}
|
|
203
|
-
const data = (await res.json()) as unknown;
|
|
204
|
-
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
205
|
-
throw new Error('voice manifest: expected JSON object');
|
|
206
|
-
}
|
|
207
|
-
const manifest: VoiceManifest = {};
|
|
208
|
-
for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
|
|
209
|
-
if (Array.isArray(value) && value.every((v) => typeof v === 'string')) {
|
|
210
|
-
manifest[key] = value as string[];
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return manifest;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function resolveSelectedDir(catalog: VoiceCatalogEntry[], pref: VoicePref | null): string {
|
|
217
|
-
if (pref && 'dir' in pref) {
|
|
218
|
-
const found = catalog.find((e) => e.dir === pref.dir);
|
|
219
|
-
if (found) return found.dir;
|
|
220
|
-
}
|
|
221
|
-
return catalog[0]!.dir;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Loads catalog + user preference + optional per-voice manifest.
|
|
226
|
-
* When pref is `{ disabled: true }`, returns `enabled: false` and `manifest: null`.
|
|
227
|
-
*/
|
|
228
|
-
export async function loadVoiceRuntime(catalogUrl: string): Promise<VoiceRuntime | null> {
|
|
229
|
-
const resolved = resolveVoiceCatalogUrl(catalogUrl);
|
|
230
|
-
if (!resolved) return null;
|
|
231
|
-
|
|
232
|
-
const catalog = await fetchVoiceCatalog(resolved);
|
|
233
|
-
const pref = await readVoicePref();
|
|
234
|
-
const selectedDir = resolveSelectedDir(catalog, pref);
|
|
235
|
-
const clipBaseUrl = voiceDirBaseUrl(resolved, selectedDir);
|
|
236
|
-
|
|
237
|
-
if (pref != null && 'disabled' in pref && pref.disabled === true) {
|
|
238
|
-
return {
|
|
239
|
-
catalogUrl: resolved,
|
|
240
|
-
catalog,
|
|
241
|
-
selectedDir,
|
|
242
|
-
clipBaseUrl,
|
|
243
|
-
manifest: null,
|
|
244
|
-
enabled: false,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const manifest = await fetchVoiceManifest(clipBaseUrl);
|
|
249
|
-
return {
|
|
250
|
-
catalogUrl: resolved,
|
|
251
|
-
catalog,
|
|
252
|
-
selectedDir,
|
|
253
|
-
clipBaseUrl,
|
|
254
|
-
manifest,
|
|
255
|
-
enabled: true,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** Reload manifest after user picks another voice (same catalog URL). */
|
|
260
|
-
export async function reloadVoiceForDir(
|
|
261
|
-
catalogUrl: string,
|
|
262
|
-
catalog: VoiceCatalogEntry[],
|
|
263
|
-
dir: string
|
|
264
|
-
): Promise<Pick<VoiceRuntime, 'selectedDir' | 'clipBaseUrl' | 'manifest' | 'enabled'>> {
|
|
265
|
-
const entry = catalog.find((e) => e.dir === dir);
|
|
266
|
-
if (!entry) {
|
|
267
|
-
throw new Error(`voice dir not in catalog: ${dir}`);
|
|
268
|
-
}
|
|
269
|
-
const clipBaseUrl = voiceDirBaseUrl(catalogUrl, entry.dir);
|
|
270
|
-
const manifest = await fetchVoiceManifest(clipBaseUrl);
|
|
271
|
-
await writeVoicePref({ dir: entry.dir });
|
|
272
|
-
return {
|
|
273
|
-
selectedDir: entry.dir,
|
|
274
|
-
clipBaseUrl,
|
|
275
|
-
manifest,
|
|
276
|
-
enabled: true,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function pickVariant(manifest: VoiceManifest, key: VoicePhraseKey): string {
|
|
281
|
-
const vars = manifest[key];
|
|
282
|
-
if (!vars?.length) return 'var1';
|
|
283
|
-
return vars[Math.floor(Math.random() * vars.length)] ?? 'var1';
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** clipId для S3: `{key}_{variant}`. */
|
|
287
|
-
function phraseKeyToClipId(manifest: VoiceManifest, key: VoicePhraseKey, variant: string): string {
|
|
288
|
-
return `${key}_${variant}`;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Преобразует список ключей фраз (из веб-части) в список URL клипов
|
|
293
|
-
* `{clipBaseUrl}/{key}_{variant}.mp3`. Неизвестные / отсутствующие в
|
|
294
|
-
* манифесте ключи пропускаются.
|
|
295
|
-
*/
|
|
296
|
-
export function keysToClipUrls(
|
|
297
|
-
manifest: VoiceManifest,
|
|
298
|
-
clipBaseUrl: string,
|
|
299
|
-
keys: string[]
|
|
300
|
-
): string[] {
|
|
301
|
-
const base = trimTrailingSlashes(clipBaseUrl);
|
|
302
|
-
const variants = new Map<VoicePhraseKey, string>();
|
|
303
|
-
const urls: string[] = [];
|
|
304
|
-
for (const raw of keys) {
|
|
305
|
-
if (!isKnownPhraseKey(raw)) continue;
|
|
306
|
-
const key = raw;
|
|
307
|
-
if (!(key in manifest)) continue;
|
|
308
|
-
let variant = variants.get(key);
|
|
309
|
-
if (!variant) {
|
|
310
|
-
variant = pickVariant(manifest, key);
|
|
311
|
-
variants.set(key, variant);
|
|
312
|
-
}
|
|
313
|
-
urls.push(`${base}/${phraseKeyToClipId(manifest, key, variant)}.mp3`);
|
|
314
|
-
}
|
|
315
|
-
return urls;
|
|
316
|
-
}
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Фиксированные ключи клипов навигатора (схема проекта). Зеркало
|
|
3
|
-
* `webProject/src/map/navigationVoiceKeys.ts` — веб-часть присылает эти
|
|
4
|
-
* ключи в событии `navigatorVoice`, нативная часть мапит их в аудио.
|
|
5
|
-
* Варианты (`var1`, …) и наличие записей — только из S3 voice manifest.
|
|
6
|
-
*/
|
|
7
|
-
export const VOICE_PHRASE_KEYS = [
|
|
8
|
-
'maneuver_sharp_left',
|
|
9
|
-
'maneuver_turn_left',
|
|
10
|
-
'maneuver_slight_left',
|
|
11
|
-
'maneuver_straight',
|
|
12
|
-
'maneuver_slight_right',
|
|
13
|
-
'maneuver_turn_right',
|
|
14
|
-
'maneuver_sharp_right',
|
|
15
|
-
'maneuver_arrival',
|
|
16
|
-
'maneuver_waypoint',
|
|
17
|
-
'maneuver_enter_roundabout',
|
|
18
|
-
'maneuver_leave_roundabout',
|
|
19
|
-
'maneuver_keep_left',
|
|
20
|
-
'maneuver_keep_right',
|
|
21
|
-
'maneuver_uturn',
|
|
22
|
-
'maneuver_uturn_left',
|
|
23
|
-
'maneuver_uturn_right',
|
|
24
|
-
'maneuver_default',
|
|
25
|
-
'exit_1',
|
|
26
|
-
'exit_2',
|
|
27
|
-
'exit_3',
|
|
28
|
-
'exit_4',
|
|
29
|
-
'exit_5',
|
|
30
|
-
'exit_6',
|
|
31
|
-
'exit_7',
|
|
32
|
-
'exit_8',
|
|
33
|
-
'exit_9',
|
|
34
|
-
'exit_10',
|
|
35
|
-
'exit_11',
|
|
36
|
-
'exit_12',
|
|
37
|
-
'roundabout_take_exit',
|
|
38
|
-
'prefix_via',
|
|
39
|
-
'connector_and',
|
|
40
|
-
'unit_meter',
|
|
41
|
-
'unit_meters_2_4',
|
|
42
|
-
'unit_meters_5_plus',
|
|
43
|
-
'unit_kilometer',
|
|
44
|
-
'unit_kilometers_2_4',
|
|
45
|
-
'unit_kilometers_5_plus',
|
|
46
|
-
'route_started',
|
|
47
|
-
'route_recalculated',
|
|
48
|
-
'arrived_title',
|
|
49
|
-
'arrival_at_destination',
|
|
50
|
-
'waypoint_reached',
|
|
51
|
-
'off_route',
|
|
52
|
-
'num_1',
|
|
53
|
-
'num_2',
|
|
54
|
-
'num_3',
|
|
55
|
-
'num_4',
|
|
56
|
-
'num_5',
|
|
57
|
-
'num_6',
|
|
58
|
-
'num_7',
|
|
59
|
-
'num_8',
|
|
60
|
-
'num_9',
|
|
61
|
-
'num_10',
|
|
62
|
-
'num_11',
|
|
63
|
-
'num_12',
|
|
64
|
-
'num_13',
|
|
65
|
-
'num_14',
|
|
66
|
-
'num_15',
|
|
67
|
-
'num_16',
|
|
68
|
-
'num_17',
|
|
69
|
-
'num_18',
|
|
70
|
-
'num_19',
|
|
71
|
-
'num_20',
|
|
72
|
-
'num_30',
|
|
73
|
-
'num_40',
|
|
74
|
-
'num_50',
|
|
75
|
-
'num_60',
|
|
76
|
-
'num_70',
|
|
77
|
-
'num_80',
|
|
78
|
-
'num_90',
|
|
79
|
-
'num_100',
|
|
80
|
-
'num_200',
|
|
81
|
-
'num_300',
|
|
82
|
-
'num_400',
|
|
83
|
-
'num_500',
|
|
84
|
-
'num_600',
|
|
85
|
-
'num_700',
|
|
86
|
-
'num_800',
|
|
87
|
-
'num_900',
|
|
88
|
-
'num_km_1',
|
|
89
|
-
'num_km_2',
|
|
90
|
-
'num_km_3',
|
|
91
|
-
'num_km_4',
|
|
92
|
-
'num_km_5',
|
|
93
|
-
'num_km_6',
|
|
94
|
-
'num_km_7',
|
|
95
|
-
'num_km_8',
|
|
96
|
-
'num_km_9',
|
|
97
|
-
'num_km_10',
|
|
98
|
-
'num_km_11',
|
|
99
|
-
'num_km_12',
|
|
100
|
-
'num_km_13',
|
|
101
|
-
'num_km_14',
|
|
102
|
-
'num_km_15',
|
|
103
|
-
'num_km_16',
|
|
104
|
-
'num_km_17',
|
|
105
|
-
'num_km_18',
|
|
106
|
-
'num_km_19',
|
|
107
|
-
'num_km_20',
|
|
108
|
-
'num_km_30',
|
|
109
|
-
'num_km_40',
|
|
110
|
-
'num_km_50',
|
|
111
|
-
'num_km_60',
|
|
112
|
-
'num_km_70',
|
|
113
|
-
'num_km_80',
|
|
114
|
-
'num_km_90',
|
|
115
|
-
'num_km_100',
|
|
116
|
-
'num_km_200',
|
|
117
|
-
'num_km_300',
|
|
118
|
-
'num_km_400',
|
|
119
|
-
'num_km_500',
|
|
120
|
-
'num_km_600',
|
|
121
|
-
'num_km_700',
|
|
122
|
-
'num_km_800',
|
|
123
|
-
'num_km_900',
|
|
124
|
-
] as const;
|
|
125
|
-
|
|
126
|
-
export type VoicePhraseKey = (typeof VOICE_PHRASE_KEYS)[number];
|
|
127
|
-
|
|
128
|
-
const VOICE_PHRASE_KEY_SET = new Set<string>(VOICE_PHRASE_KEYS);
|
|
129
|
-
|
|
130
|
-
export function isKnownPhraseKey(key: string): key is VoicePhraseKey {
|
|
131
|
-
return VOICE_PHRASE_KEY_SET.has(key);
|
|
132
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
loadVoiceRuntime,
|
|
5
|
-
readVoiceVolumeLevel,
|
|
6
|
-
reloadVoiceForDir,
|
|
7
|
-
writeVoicePref,
|
|
8
|
-
writeVoiceVolumeLevel,
|
|
9
|
-
type VoiceCatalogEntry,
|
|
10
|
-
type VoiceManifest,
|
|
11
|
-
type VoiceRuntime,
|
|
12
|
-
type VoiceVolumeLevel,
|
|
13
|
-
} from './navigatorVoiceCatalog';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Нативное владение голосом навигатора: загрузка каталога/манифеста,
|
|
17
|
-
* выбор голоса/громкости и персист (AsyncStorage). Веб шлёт только ключи
|
|
18
|
-
* фраз; маппинг в аудио и воспроизведение — на стороне RN.
|
|
19
|
-
*/
|
|
20
|
-
export type NavigatorVoiceController = {
|
|
21
|
-
runtime: VoiceRuntime | null;
|
|
22
|
-
catalog: VoiceCatalogEntry[];
|
|
23
|
-
selectedDir: string | null;
|
|
24
|
-
manifest: VoiceManifest | null;
|
|
25
|
-
clipBaseUrl: string | null;
|
|
26
|
-
voiceEnabled: boolean;
|
|
27
|
-
volumeLevel: VoiceVolumeLevel;
|
|
28
|
-
selectVoice: (dir: string) => Promise<void>;
|
|
29
|
-
selectVolume: (level: VoiceVolumeLevel) => void;
|
|
30
|
-
disableVoice: () => Promise<void>;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export function useNavigatorVoice(voiceUrl: string | undefined): NavigatorVoiceController {
|
|
34
|
-
const [runtime, setRuntime] = useState<VoiceRuntime | null>(null);
|
|
35
|
-
const [volumeLevel, setVolumeLevel] = useState<VoiceVolumeLevel>(5);
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
let cancelled = false;
|
|
39
|
-
void readVoiceVolumeLevel().then((lvl) => {
|
|
40
|
-
if (!cancelled) setVolumeLevel(lvl);
|
|
41
|
-
});
|
|
42
|
-
if (!voiceUrl) {
|
|
43
|
-
setRuntime(null);
|
|
44
|
-
return () => {
|
|
45
|
-
cancelled = true;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
void loadVoiceRuntime(voiceUrl)
|
|
49
|
-
.then((rt) => {
|
|
50
|
-
if (!cancelled) setRuntime(rt);
|
|
51
|
-
})
|
|
52
|
-
.catch((err) => {
|
|
53
|
-
console.warn('[maplite] navigator voice catalog failed', err);
|
|
54
|
-
if (!cancelled) setRuntime(null);
|
|
55
|
-
});
|
|
56
|
-
return () => {
|
|
57
|
-
cancelled = true;
|
|
58
|
-
};
|
|
59
|
-
}, [voiceUrl]);
|
|
60
|
-
|
|
61
|
-
const selectVoice = useCallback(
|
|
62
|
-
async (dir: string) => {
|
|
63
|
-
if (!runtime) return;
|
|
64
|
-
try {
|
|
65
|
-
const reloaded = await reloadVoiceForDir(runtime.catalogUrl, runtime.catalog, dir);
|
|
66
|
-
setRuntime((prev) => (prev ? { ...prev, ...reloaded } : prev));
|
|
67
|
-
} catch (err) {
|
|
68
|
-
console.warn('[maplite] failed to switch navigator voice', { dir, err });
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
[runtime]
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
const selectVolume = useCallback((level: VoiceVolumeLevel) => {
|
|
75
|
-
setVolumeLevel(level);
|
|
76
|
-
void writeVoiceVolumeLevel(level);
|
|
77
|
-
}, []);
|
|
78
|
-
|
|
79
|
-
const disableVoice = useCallback(async () => {
|
|
80
|
-
await writeVoicePref({ disabled: true });
|
|
81
|
-
setRuntime((prev) => (prev ? { ...prev, enabled: false, manifest: null } : prev));
|
|
82
|
-
}, []);
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
runtime,
|
|
86
|
-
catalog: runtime?.catalog ?? [],
|
|
87
|
-
selectedDir: runtime?.selectedDir ?? null,
|
|
88
|
-
manifest: runtime?.manifest ?? null,
|
|
89
|
-
clipBaseUrl: runtime?.clipBaseUrl ?? null,
|
|
90
|
-
voiceEnabled: Boolean(runtime?.enabled && runtime.manifest),
|
|
91
|
-
volumeLevel,
|
|
92
|
-
selectVoice,
|
|
93
|
-
selectVolume,
|
|
94
|
-
disableVoice,
|
|
95
|
-
};
|
|
96
|
-
}
|