@swiftbrowser/native 0.18.0 → 0.18.2
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 +73 -8
- package/dist/bridge.d.ts +64 -2
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/geolocation.d.ts +73 -0
- package/dist/geolocation.d.ts.map +1 -0
- package/dist/geolocation.js +109 -0
- package/dist/geolocation.js.map +1 -0
- package/dist/hostRequests.d.ts +3 -1
- package/dist/hostRequests.d.ts.map +1 -1
- package/dist/hostRequests.js +46 -5
- package/dist/hostRequests.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/nativeFunctions.d.ts +35 -0
- package/dist/nativeFunctions.d.ts.map +1 -0
- package/dist/nativeFunctions.js +90 -0
- package/dist/nativeFunctions.js.map +1 -0
- package/dist/reachability.d.ts +24 -0
- package/dist/reachability.d.ts.map +1 -0
- package/dist/reachability.js +41 -0
- package/dist/reachability.js.map +1 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +3 -1
- package/dist/run.js.map +1 -1
- package/dist/runtime.d.ts +9 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +62 -5
- package/dist/runtime.js.map +1 -1
- package/dist/speech.d.ts +148 -0
- package/dist/speech.d.ts.map +1 -0
- package/dist/speech.js +243 -0
- package/dist/speech.js.map +1 -0
- package/dist/wakeLock.d.ts +32 -0
- package/dist/wakeLock.d.ts.map +1 -0
- package/dist/wakeLock.js +60 -0
- package/dist/wakeLock.js.map +1 -0
- package/package.json +3 -3
- package/src/bridge.ts +70 -2
- package/src/geolocation.ts +160 -0
- package/src/hostRequests.ts +47 -7
- package/src/index.ts +1 -0
- package/src/nativeFunctions.ts +94 -0
- package/src/reachability.ts +49 -0
- package/src/run.ts +5 -3
- package/src/runtime.ts +58 -5
- package/src/speech.ts +312 -0
- package/src/wakeLock.ts +63 -0
- package/swift/Sources/SwiftBrowserNative/Host/AppleSignInBridge.swift +139 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioRecorderBridge.swift +5 -0
- package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +83 -12
- package/swift/Sources/SwiftBrowserNative/Host/GeolocationBridge.swift +150 -0
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +131 -2
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +81 -11
- package/swift/Sources/SwiftBrowserNative/Host/ICloudBridge.swift +112 -0
- package/swift/Sources/SwiftBrowserNative/Host/ReachabilityBridge.swift +47 -0
- package/swift/Sources/SwiftBrowserNative/Host/SpeechBridge.swift +106 -0
- package/swift/Sources/SwiftBrowserNative/Host/SpeechRecognitionBridge.swift +198 -0
- package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +14 -5
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +134 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +1 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +2 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextInputHints.swift +105 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Touches.swift +102 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +13 -0
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +9 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
package/src/hostRequests.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The host requests `@swiftbrowser/react` sends to the browser environment
|
|
3
|
-
* (`ShareLink`, `fileImporter`, the clipboard, a color chooser: the web
|
|
3
|
+
* (`ShareLink`, `fileImporter`, `fileExporter`, the clipboard, a color chooser: the web
|
|
4
4
|
* renderer's `performHostRequest` with its `browserEnvironment`), answered by
|
|
5
5
|
* the native app instead: the bridge's `hostRequest(id, kind, json)` performs
|
|
6
6
|
* one with UIKit (a share sheet, the photo or document picker, the
|
|
@@ -47,6 +47,7 @@ const pending = new Map<number, { resolve: (value: unknown) => void; reject: (er
|
|
|
47
47
|
/** Points the requests at the bridge (the runtime, once per install). */
|
|
48
48
|
export function installHostRequests(native: HostRequestBridge): void {
|
|
49
49
|
bridge = native;
|
|
50
|
+
fileWatchers.clear();
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/** The bridge's `__sbHostResponse(id, result)`. */
|
|
@@ -135,25 +136,54 @@ function appDirectories(): FileDirectories | null {
|
|
|
135
136
|
return null;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
/**
|
|
139
|
+
/** Who hears about the files iCloud Drive changed (`watch`). */
|
|
140
|
+
const fileWatchers = new Set<(urls: string[]) => void>();
|
|
141
|
+
|
|
142
|
+
/** The bridge's `__sbFilesChanged(json)`: the URLs another device changed in iCloud Drive (ICloudBridge.swift). */
|
|
143
|
+
export function receiveFilesChanged(json: string | string[]): void {
|
|
144
|
+
let urls: string[];
|
|
145
|
+
try {
|
|
146
|
+
const parsed: unknown = typeof json === 'string' ? JSON.parse(json) : json;
|
|
147
|
+
urls = Array.isArray(parsed) ? parsed.filter((url): url is string => typeof url === 'string') : [];
|
|
148
|
+
} catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (urls.length === 0) return;
|
|
152
|
+
for (const listener of [...fileWatchers]) listener(urls);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The app's files (`files` requests): `fileManager`'s host over the sandbox, and iCloud Drive's container once asked for. */
|
|
139
156
|
function nativeFiles(directories: FileDirectories): Record<string, unknown> {
|
|
140
157
|
const files = <T>(params: Record<string, unknown>): Promise<T> => hostRequest<T>('files', params);
|
|
158
|
+
let iCloud: Promise<string | null> | null = null;
|
|
141
159
|
return {
|
|
142
160
|
directories,
|
|
161
|
+
// The container's Documents (ICloudBridge.swift), resolved once; null without the entitlement or iCloud Drive.
|
|
162
|
+
iCloud: (): Promise<string | null> => (iCloud ??= files<{ url?: string } | null>({ action: 'iCloud' }).then((found) => (typeof found?.url === 'string' ? found.url : null))),
|
|
163
|
+
watch: (listener: (urls: string[]) => void): (() => void) => {
|
|
164
|
+
fileWatchers.add(listener);
|
|
165
|
+
return () => void fileWatchers.delete(listener);
|
|
166
|
+
},
|
|
143
167
|
read: async (url: string): Promise<Uint8Array> => base64Decode((await files<{ data?: string }>({ action: 'read', url }))?.data ?? ''),
|
|
144
168
|
write: (url: string, data: Uint8Array): Promise<void> => files({ action: 'write', url, data: base64Encode(data) }).then(() => undefined),
|
|
145
169
|
remove: (url: string): Promise<void> => files({ action: 'remove', url }).then(() => undefined),
|
|
146
170
|
createDirectory: (url: string): Promise<void> => files({ action: 'createDirectory', url }).then(() => undefined),
|
|
147
171
|
list: async (url: string): Promise<string[]> => (await files<{ urls?: string[] }>({ action: 'list', url }))?.urls ?? [],
|
|
148
|
-
attributes: async (url: string): Promise<{ size: number; modified: number | null; directory: boolean } | null> => {
|
|
149
|
-
const found = await files<{ size?: number; modified?: number | null; directory?: boolean } | null>({ action: 'attributes', url });
|
|
172
|
+
attributes: async (url: string): Promise<{ size: number; modified: number | null; created: number | null; directory: boolean; type: string } | null> => {
|
|
173
|
+
const found = await files<{ size?: number; modified?: number | null; created?: number | null; directory?: boolean; type?: string } | null>({ action: 'attributes', url });
|
|
150
174
|
if (!found || typeof found.size !== 'number') return null;
|
|
151
|
-
return {
|
|
175
|
+
return {
|
|
176
|
+
size: found.size,
|
|
177
|
+
modified: typeof found.modified === 'number' ? found.modified : null,
|
|
178
|
+
created: typeof found.created === 'number' ? found.created : null,
|
|
179
|
+
directory: found.directory === true,
|
|
180
|
+
type: typeof found.type === 'string' ? found.type : '',
|
|
181
|
+
};
|
|
152
182
|
},
|
|
153
183
|
move: (from: string, to: string): Promise<void> => files({ action: 'move', from, to }).then(() => undefined),
|
|
154
184
|
copy: (from: string, to: string): Promise<void> => files({ action: 'copy', from, to }).then(() => undefined),
|
|
155
|
-
// AVPlayer and UIImage read the file themselves.
|
|
156
|
-
loadURL: async (url: string): Promise<string> => url,
|
|
185
|
+
// AVPlayer and UIImage read the file themselves, once it is on the device (a file another device put in iCloud Drive is downloaded first).
|
|
186
|
+
loadURL: async (url: string): Promise<string> => (await files<{ url?: string } | null>({ action: 'download', url }))?.url ?? url,
|
|
157
187
|
};
|
|
158
188
|
}
|
|
159
189
|
|
|
@@ -180,6 +210,11 @@ export function nativeHostEnvironment(): Record<string, unknown> {
|
|
|
180
210
|
void hostRequest('share', { files: [{ name: filename, type: blob.type, data: base64Encode(blob.bytes()) }] }).catch(() => undefined);
|
|
181
211
|
},
|
|
182
212
|
pickFiles: async (options: { accept: string; multiple: boolean }): Promise<SimpleFile[]> => filesOf(await hostRequest('pickFiles', { accept: options.accept, multiple: options.multiple })),
|
|
213
|
+
// `fileExporter`: the document picker, saving the files where the viewer chooses.
|
|
214
|
+
exportFiles: async (files: SimpleFile[]): Promise<{ outcome: string; urls?: string[] }> => {
|
|
215
|
+
const result = await hostRequest<{ outcome?: string; urls?: string[] } | null>('exportFiles', { files: await Promise.all(files.map(bridgedFile)) });
|
|
216
|
+
return { outcome: result?.outcome ?? 'cancelled', ...(result?.urls ? { urls: result.urls } : {}) };
|
|
217
|
+
},
|
|
183
218
|
objectURL: (file: SimpleBlob): string => objectURL(file),
|
|
184
219
|
clipboard: {
|
|
185
220
|
writeText: (text: string): Promise<void> => hostRequest('clipboard', { action: 'write', text }).then(() => undefined),
|
|
@@ -190,6 +225,11 @@ export function nativeHostEnvironment(): Record<string, unknown> {
|
|
|
190
225
|
readText: async (): Promise<string> => (await hostRequest<{ text?: string }>('clipboard', { action: 'readText' }))?.text ?? '',
|
|
191
226
|
},
|
|
192
227
|
pickColor: async (color: string): Promise<string | null> => (await hostRequest<{ color?: string }>('pickColor', { color }))?.color ?? null,
|
|
228
|
+
// Sign in with Apple (AppleSignInBridge.swift): the system sheet, and the credential's state.
|
|
229
|
+
appleSignIn: {
|
|
230
|
+
signIn: (options: { scopes: string[]; nonce?: string; state?: string }): Promise<unknown> => hostRequest('appleSignIn', { action: 'signIn', ...options }),
|
|
231
|
+
credentialState: async (user: string): Promise<string> => (await hostRequest<{ state?: string } | null>('appleSignIn', { action: 'credentialState', user }))?.state ?? 'unknown',
|
|
232
|
+
},
|
|
193
233
|
// `AVAudioRecorder` (AudioRecorderBridge.swift): a recording ends as a file URL in Documents; `trim` writes another.
|
|
194
234
|
audioRecorder: {
|
|
195
235
|
start: (): Promise<void> => hostRequest('audioRecord', { action: 'start' }).then(() => undefined),
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
export { run, type RunOptions, type Running } from './run';
|
|
15
15
|
export { Native, type NativeProps } from './native';
|
|
16
|
+
export { callNative, callNativeSync, nativeFunctions, hasNativeFunction, onNativeEvent } from './nativeFunctions';
|
|
16
17
|
export { installRuntime, type InstallOptions } from './runtime';
|
|
17
18
|
export { nativeBridge, launchArguments, type NativeBridge, type NativeFetchRequest, type NativeFetchResult, type WorkerEngine } from './bridge';
|
|
18
19
|
export { SimpleHeaders, SimpleRequest, SimpleResponse, SimpleAbortController, SimpleAbortSignal, createFetch } from './fetch';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swift the app calls: the function half of the escape hatch (docs/native.md;
|
|
3
|
+
* `<Native>` in native.ts is the view half). The Swift app registers
|
|
4
|
+
* functions with `NativeFunctions.register(name) { call in ... }`
|
|
5
|
+
* (Native/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift);
|
|
6
|
+
* `callNative(name, params)` runs one and resolves with what it returned,
|
|
7
|
+
* through the host request `app:<name>` (hostRequests.ts), `callNativeSync`
|
|
8
|
+
* runs a sync one inline over the bridge's `callNativeSync(name, json)`, and
|
|
9
|
+
* `nativeFunctions()` lists them over `nativeFunctions()`. What Swift sends
|
|
10
|
+
* with `NativeFunctions.send(name, value)` arrives at `__sbNativeEvent(name,
|
|
11
|
+
* json)` and reaches `onNativeEvent(name)`'s listeners.
|
|
12
|
+
*
|
|
13
|
+
* On a page, or in a host without the functions, `callNative` rejects and
|
|
14
|
+
* `callNativeSync` throws `unsupported:`, and `nativeFunctions()` is empty:
|
|
15
|
+
* an app checks `hasNativeFunction(name)` and falls back to the web's way.
|
|
16
|
+
* A function that throws `NativeFunctionCancelled` rejects with an
|
|
17
|
+
* `AbortError`, as the system pickers' cancellations do.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { NativeBridge } from './bridge';
|
|
21
|
+
import { hostRequest } from './hostRequests';
|
|
22
|
+
|
|
23
|
+
export type NativeFunctionsBridge = Pick<NativeBridge, 'nativeFunctions' | 'callNativeSync'>;
|
|
24
|
+
|
|
25
|
+
let bridge: NativeFunctionsBridge | null = null;
|
|
26
|
+
const listeners = new Map<string, Set<(value: unknown) => void>>();
|
|
27
|
+
|
|
28
|
+
/** Points the functions at the bridge (the runtime, once per install); null forgets it. */
|
|
29
|
+
export function installNativeFunctions(native: NativeFunctionsBridge | null): void {
|
|
30
|
+
bridge = native;
|
|
31
|
+
listeners.clear();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The names the Swift app registered; empty on a page. */
|
|
35
|
+
export function nativeFunctions(): string[] {
|
|
36
|
+
return bridge?.nativeFunctions?.() ?? [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether the Swift app registered `name`. */
|
|
40
|
+
export function hasNativeFunction(name: string): boolean {
|
|
41
|
+
return nativeFunctions().includes(name);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Runs the function registered under `name` with `params`; resolves with what it returned. */
|
|
45
|
+
export function callNative<T = unknown>(name: string, params: Record<string, unknown> = {}): Promise<T> {
|
|
46
|
+
return hostRequest<T>(`app:${name}`, params);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Runs the sync function registered under `name` inline; returns what it returned. */
|
|
50
|
+
export function callNativeSync<T = unknown>(name: string, params: Record<string, unknown> = {}): T {
|
|
51
|
+
if (!bridge?.callNativeSync) throw new Error(`unsupported: no native host for ${name}`);
|
|
52
|
+
let result: { value?: unknown; error?: string };
|
|
53
|
+
try {
|
|
54
|
+
result = JSON.parse(bridge.callNativeSync(name, JSON.stringify(params))) as { value?: unknown; error?: string };
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error(`${name}: malformed response`);
|
|
57
|
+
}
|
|
58
|
+
if (typeof result.error === 'string') {
|
|
59
|
+
const error = new Error(result.error);
|
|
60
|
+
if (result.error.startsWith('cancelled')) error.name = 'AbortError';
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
return result.value as T;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Listens for what Swift sends under `name` (`NativeFunctions.send`); returns the function that stops listening. */
|
|
67
|
+
export function onNativeEvent<T = unknown>(name: string, listener: (value: T) => void): () => void {
|
|
68
|
+
let set = listeners.get(name);
|
|
69
|
+
if (!set) {
|
|
70
|
+
set = new Set();
|
|
71
|
+
listeners.set(name, set);
|
|
72
|
+
}
|
|
73
|
+
const entry = listener as (value: unknown) => void;
|
|
74
|
+
set.add(entry);
|
|
75
|
+
return () => {
|
|
76
|
+
set.delete(entry);
|
|
77
|
+
if (set.size === 0) listeners.delete(name);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The bridge's `__sbNativeEvent(name, json)`. */
|
|
82
|
+
export function receiveNativeEvent(name: string, value: unknown): void {
|
|
83
|
+
const set = listeners.get(name);
|
|
84
|
+
if (!set) return;
|
|
85
|
+
let parsed = value;
|
|
86
|
+
if (typeof value === 'string') {
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(value);
|
|
89
|
+
} catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const listener of [...set]) listener(parsed);
|
|
94
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `navigator.onLine` and the `online` and `offline` events over the host's
|
|
3
|
+
* path monitor (`online()` on the bridge, `__sbOnline(bool)` back). The
|
|
4
|
+
* value is what the host last said (`NWPathMonitor`'s path status, true
|
|
5
|
+
* where the host has no monitor); the events reach listeners on the global
|
|
6
|
+
* (`addEventListener('online', …)`, which the runtime defines in the app's
|
|
7
|
+
* context, where a page's `window` would take them) and the global's
|
|
8
|
+
* `ononline` and `onoffline` handlers.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { NativeBridge } from './bridge';
|
|
12
|
+
|
|
13
|
+
type Listener = (event: { type: 'online' | 'offline' }) => void;
|
|
14
|
+
|
|
15
|
+
export interface Reachability {
|
|
16
|
+
/** `navigator.onLine`. */
|
|
17
|
+
readonly onLine: boolean;
|
|
18
|
+
addEventListener(type: string, listener: Listener): void;
|
|
19
|
+
removeEventListener(type: string, listener: Listener): void;
|
|
20
|
+
/** `__sbOnline(online)`: the host's path changed. */
|
|
21
|
+
receive(online: boolean): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createReachability(native: NativeBridge, target: Record<string, unknown>): Reachability {
|
|
25
|
+
let online = native.online?.() ?? true;
|
|
26
|
+
const listeners = { online: new Set<Listener>(), offline: new Set<Listener>() };
|
|
27
|
+
const isKind = (type: string): type is 'online' | 'offline' => type === 'online' || type === 'offline';
|
|
28
|
+
return {
|
|
29
|
+
get onLine() {
|
|
30
|
+
return online;
|
|
31
|
+
},
|
|
32
|
+
addEventListener(type, listener) {
|
|
33
|
+
if (isKind(type) && typeof listener === 'function') listeners[type].add(listener);
|
|
34
|
+
},
|
|
35
|
+
removeEventListener(type, listener) {
|
|
36
|
+
if (isKind(type)) listeners[type].delete(listener);
|
|
37
|
+
},
|
|
38
|
+
receive(next) {
|
|
39
|
+
const value = Boolean(next);
|
|
40
|
+
if (value === online) return;
|
|
41
|
+
online = value;
|
|
42
|
+
const type = value ? 'online' : 'offline';
|
|
43
|
+
const event = { type } as const;
|
|
44
|
+
const handler = target[value ? 'ononline' : 'onoffline'];
|
|
45
|
+
if (typeof handler === 'function') (handler as Listener)(event);
|
|
46
|
+
for (const listener of [...listeners[type]]) listener(event);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
package/src/run.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ran and its pass was applied (docs/native.md, "Synchronous calls").
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { createSwiftUIRoot, installTextMeasurer, type SwiftUIRoot, type TextMeasurerFunction } from '@swiftbrowser/react';
|
|
11
|
+
import { createSwiftUIRoot, installTextMeasurer, type Environment, type SwiftUIRoot, type TextMeasurerFunction } from '@swiftbrowser/react';
|
|
12
12
|
import type { Op, RendererEvent } from '@swiftbrowser/web/src/protocol';
|
|
13
13
|
import type { ReactNode } from 'react';
|
|
14
14
|
import { browserEnvironment } from '@swiftbrowser/web/src/hostRequests';
|
|
@@ -35,6 +35,7 @@ interface EnvironmentFields {
|
|
|
35
35
|
colorScheme?: 'light' | 'dark';
|
|
36
36
|
dynamicTypeSize?: string;
|
|
37
37
|
horizontalSizeClass?: 'compact' | 'regular';
|
|
38
|
+
scenePhase?: 'active' | 'inactive' | 'background';
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/** The running app, when `run` was called (the dev loop calls it again on every save). */
|
|
@@ -82,13 +83,14 @@ export function run(element: ReactNode, options: RunOptions = {}): Running {
|
|
|
82
83
|
try {
|
|
83
84
|
if (parsed.type === 'environment') {
|
|
84
85
|
const fields = parsed as EnvironmentFields;
|
|
85
|
-
const next: Partial<
|
|
86
|
+
const next: Partial<Environment> = {};
|
|
86
87
|
if (fields.colorScheme) {
|
|
87
88
|
next.colorScheme = fields.colorScheme;
|
|
88
89
|
updateMediaEnvironment({ colorScheme: fields.colorScheme });
|
|
89
90
|
}
|
|
90
|
-
if (fields.dynamicTypeSize)
|
|
91
|
+
if (fields.dynamicTypeSize) next.dynamicTypeSize = fields.dynamicTypeSize as Environment['dynamicTypeSize'];
|
|
91
92
|
if (fields.horizontalSizeClass) next.horizontalSizeClass = fields.horizontalSizeClass;
|
|
93
|
+
if (fields.scenePhase) next.scenePhase = fields.scenePhase;
|
|
92
94
|
if (Object.keys(next).length > 0) root.setEnvironment(next);
|
|
93
95
|
return;
|
|
94
96
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -11,12 +11,19 @@
|
|
|
11
11
|
* `atob`, `btoa`, `fetch` with `Headers`, `Request`, `Response`,
|
|
12
12
|
* `AbortController` and `AbortSignal`, `URL`, `URLSearchParams`, `crypto`
|
|
13
13
|
* (`getRandomValues`, `randomUUID`), `localStorage`, `structuredClone`,
|
|
14
|
-
* `navigator` (with `
|
|
14
|
+
* `navigator` (with `onLine` from the host's path monitor, reachability.ts,
|
|
15
|
+
* whose `online` and `offline` events `addEventListener` on the global takes
|
|
16
|
+
* with the bridge's `__sbOnline`; with `wakeLock` and `WakeLockSentinel` on
|
|
17
|
+
* a bridge with `wakeLock`, wakeLock.ts; with `geolocation` and
|
|
18
|
+
* `GeolocationPositionError` on a bridge with `geolocation`, geolocation.ts,
|
|
19
|
+
* with the bridge's `__sbGeolocation`; with `mediaSession` and `MediaMetadata` on a bridge with
|
|
15
20
|
* `mediaSession`, mediaSession.ts), `Image` (image.ts, on a bridge with `probeImage`) with the
|
|
16
21
|
* bridge's `__sbImageProbed`, `Worker` (worker.ts) with the bridge's
|
|
17
22
|
* `__sbWorkerMessage` and `__sbWorkerError`, `__sbOpenURL` (openURL.ts),
|
|
18
23
|
* `__sbNotificationPermission`, `__sbPushRegistered` and `__sbNotification` (notifications.ts),
|
|
19
|
-
* `__sbUndoCommand` (undo.ts, on a bridge with `undoState`),
|
|
24
|
+
* `__sbUndoCommand` (undo.ts, on a bridge with `undoState`), `speechSynthesis`,
|
|
25
|
+
* `SpeechSynthesisUtterance` and `__sbSpeechEvent` on a bridge with `speak`,
|
|
26
|
+
* `SpeechRecognition` and `__sbRecognition` on one with `recognize` (speech.ts),
|
|
20
27
|
* and the bridge's callbacks `__sbTimerFired` and `__sbFetchResult`. In a
|
|
21
28
|
* worker's own context (its bridge has
|
|
22
29
|
* `workerPostMessage`), `self`, `postMessage`, `addEventListener` and
|
|
@@ -40,7 +47,12 @@ import { NativeWebSocket } from './webSocket';
|
|
|
40
47
|
import { createSubtleCrypto } from './subtle';
|
|
41
48
|
import { matchMedia } from './matchMedia';
|
|
42
49
|
import { createUndoHost, installUndoHost } from './undo';
|
|
43
|
-
import { installHostRequests, receiveHostResponse } from './hostRequests';
|
|
50
|
+
import { installHostRequests, receiveFilesChanged, receiveHostResponse } from './hostRequests';
|
|
51
|
+
import { installNativeFunctions, receiveNativeEvent } from './nativeFunctions';
|
|
52
|
+
import { createReachability } from './reachability';
|
|
53
|
+
import { createWakeLock, WakeLockSentinel } from './wakeLock';
|
|
54
|
+
import { createGeolocation, GeolocationPositionError } from './geolocation';
|
|
55
|
+
import { createSpeechSynthesis, SpeechRecognition, SpeechSynthesisUtterance } from './speech';
|
|
44
56
|
|
|
45
57
|
type AnyGlobal = Record<string, unknown>;
|
|
46
58
|
|
|
@@ -209,6 +221,15 @@ export function installRuntime(native: NativeBridge, target: AnyGlobal = globalT
|
|
|
209
221
|
if (native.hostRequest && !native.workerPostMessage) {
|
|
210
222
|
installHostRequests({ hostRequest: native.hostRequest.bind(native), ...(native.appDirectories ? { appDirectories: native.appDirectories.bind(native) } : {}) });
|
|
211
223
|
define('__sbHostResponse', receiveHostResponse);
|
|
224
|
+
define('__sbFilesChanged', receiveFilesChanged);
|
|
225
|
+
// The escape hatch's functions (nativeFunctions.ts): `callNative` rides `hostRequest`.
|
|
226
|
+
installNativeFunctions({
|
|
227
|
+
...(native.nativeFunctions ? { nativeFunctions: native.nativeFunctions.bind(native) } : {}),
|
|
228
|
+
...(native.callNativeSync ? { callNativeSync: native.callNativeSync.bind(native) } : {}),
|
|
229
|
+
});
|
|
230
|
+
define('__sbNativeEvent', receiveNativeEvent);
|
|
231
|
+
} else {
|
|
232
|
+
installNativeFunctions(null);
|
|
212
233
|
}
|
|
213
234
|
|
|
214
235
|
// The window's undo manager (undo.ts), mirroring the app's: the system's undo comes to `__sbUndoCommand`.
|
|
@@ -259,11 +280,15 @@ export function installRuntime(native: NativeBridge, target: AnyGlobal = globalT
|
|
|
259
280
|
|
|
260
281
|
define('structuredClone', <T>(value: T): T => (value === undefined ? value : (JSON.parse(JSON.stringify(value)) as T)));
|
|
261
282
|
|
|
283
|
+
// Reachability (reachability.ts): `navigator.onLine` from the host's path monitor.
|
|
284
|
+
const reachability = createReachability(native, target);
|
|
262
285
|
const navigator: Record<string, unknown> = {
|
|
263
286
|
userAgent: 'SwiftBrowserNative',
|
|
264
287
|
language: native.locale().replace('_', '-'),
|
|
265
288
|
languages: [native.locale().replace('_', '-')],
|
|
266
|
-
onLine
|
|
289
|
+
get onLine() {
|
|
290
|
+
return reachability.onLine;
|
|
291
|
+
},
|
|
267
292
|
platform: 'iOS',
|
|
268
293
|
hardwareConcurrency: 1,
|
|
269
294
|
// A haptic for a vibration; the pattern's length does not matter.
|
|
@@ -275,6 +300,12 @@ export function installRuntime(native: NativeBridge, target: AnyGlobal = globalT
|
|
|
275
300
|
return true;
|
|
276
301
|
},
|
|
277
302
|
};
|
|
303
|
+
// The screen wake lock (wakeLock.ts) over the host's idle timer; not in a worker.
|
|
304
|
+
const wakeLock = native.wakeLock && !native.workerPostMessage ? createWakeLock(native) : null;
|
|
305
|
+
if (wakeLock) navigator['wakeLock'] = wakeLock;
|
|
306
|
+
// The viewer's position (geolocation.ts) over the host's location manager; not in a worker.
|
|
307
|
+
const geolocation = native.geolocation && !native.workerPostMessage ? createGeolocation(native, target) : null;
|
|
308
|
+
if (geolocation) navigator['geolocation'] = geolocation.geolocation;
|
|
278
309
|
// What plays, for the lock screen and the media keys (mediaSession.ts); not in a worker.
|
|
279
310
|
const mediaSession = native.mediaSession && !native.workerPostMessage ? createMediaSession(native) : null;
|
|
280
311
|
if (mediaSession) navigator['mediaSession'] = mediaSession.session;
|
|
@@ -286,6 +317,24 @@ export function installRuntime(native: NativeBridge, target: AnyGlobal = globalT
|
|
|
286
317
|
navigator['clearAppBadge'] = notifications.clearAppBadge;
|
|
287
318
|
}
|
|
288
319
|
define('navigator', navigator);
|
|
320
|
+
if (wakeLock) define('WakeLockSentinel', WakeLockSentinel);
|
|
321
|
+
if (geolocation) {
|
|
322
|
+
define('GeolocationPositionError', GeolocationPositionError);
|
|
323
|
+
define('__sbGeolocation', geolocation.receive);
|
|
324
|
+
}
|
|
325
|
+
// Speech (speech.ts): synthesis on AVSpeechSynthesizer, recognition on SFSpeechRecognizer; not in a worker.
|
|
326
|
+
if (native.speak && native.speechControl && !native.workerPostMessage) {
|
|
327
|
+
const synthesis = createSpeechSynthesis(native);
|
|
328
|
+
define('speechSynthesis', synthesis.speechSynthesis);
|
|
329
|
+
define('SpeechSynthesisUtterance', SpeechSynthesisUtterance);
|
|
330
|
+
define('__sbSpeechEvent', synthesis.receive);
|
|
331
|
+
}
|
|
332
|
+
if (native.recognize && !native.workerPostMessage) {
|
|
333
|
+
SpeechRecognition.install({ recognize: native.recognize.bind(native) });
|
|
334
|
+
define('SpeechRecognition', SpeechRecognition);
|
|
335
|
+
define('webkitSpeechRecognition', SpeechRecognition);
|
|
336
|
+
define('__sbRecognition', SpeechRecognition.receive);
|
|
337
|
+
}
|
|
289
338
|
if (mediaSession) {
|
|
290
339
|
define('MediaMetadata', NativeMediaMetadata);
|
|
291
340
|
define('__sbMediaSessionCommand', mediaSession.receive);
|
|
@@ -300,8 +349,12 @@ export function installRuntime(native: NativeBridge, target: AnyGlobal = globalT
|
|
|
300
349
|
if (native.workerPostMessage) {
|
|
301
350
|
installed.push(...installWorkerScope(native, target));
|
|
302
351
|
} else {
|
|
303
|
-
// The app's context: the URLs it is opened with (openURL.ts),
|
|
352
|
+
// The app's context: the URLs it is opened with (openURL.ts), the
|
|
353
|
+
// `online` and `offline` events (a page's `window` takes them), and workers.
|
|
304
354
|
define('__sbOpenURL', receiveOpenURL);
|
|
355
|
+
define('addEventListener', reachability.addEventListener);
|
|
356
|
+
define('removeEventListener', reachability.removeEventListener);
|
|
357
|
+
define('__sbOnline', reachability.receive);
|
|
305
358
|
if (native.workerStart) {
|
|
306
359
|
define('Worker', NativeWorker);
|
|
307
360
|
define('__sbWorkerMessage', NativeWorker.receive);
|