@ayden-fc2/riffle-bridge-web 1.0.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/src/core.ts ADDED
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Riffle Bridge 核心通信模块
3
+ *
4
+ * 处理 WebView 与 Native 之间的消息收发
5
+ */
6
+
7
+ import type { BridgeResponse } from './types';
8
+
9
+ // 超时配置
10
+ const TIMEOUT_CONFIG: Record<string, Record<string, number>> = {
11
+ fileStorage: {
12
+ downloadAndSave: 30000,
13
+ downloadWithCache: 30000,
14
+ takePhoto: 15000,
15
+ pickFromGallery: 15000,
16
+ default: 10000,
17
+ },
18
+ device: {
19
+ default: 8000,
20
+ },
21
+ default: {
22
+ default: 5000,
23
+ },
24
+ };
25
+
26
+ /**
27
+ * 获取请求超时时间
28
+ */
29
+ function getTimeout(module: string, method: string): number {
30
+ const moduleConfig = TIMEOUT_CONFIG[module] || TIMEOUT_CONFIG.default;
31
+ return moduleConfig[method] || moduleConfig.default || 5000;
32
+ }
33
+
34
+ /**
35
+ * Bridge 核心类 - 处理消息收发
36
+ */
37
+ export class BridgeCore {
38
+ private messageId = 0;
39
+ private initialized = false;
40
+ private sensorHandlers: Map<string, Set<(data: unknown[]) => void>> = new Map();
41
+
42
+ constructor() {
43
+ this.init();
44
+ }
45
+
46
+ /**
47
+ * 初始化 Bridge
48
+ */
49
+ private init(): void {
50
+ if (this.initialized) return;
51
+
52
+ // 初始化回调存储
53
+ if (!window.__RIFFLE_CALLBACKS__) {
54
+ window.__RIFFLE_CALLBACKS__ = new Map();
55
+ }
56
+
57
+ // 设置响应处理函数
58
+ window.handleNativeResponse = this.handleResponse.bind(this);
59
+
60
+ // 设置传感器数据处理函数
61
+ window.handleSensorData = this.handleSensorData.bind(this);
62
+
63
+ // 保存传感器处理器引用
64
+ window.__RIFFLE_SENSOR_HANDLERS__ = this.sensorHandlers;
65
+
66
+ this.initialized = true;
67
+ console.log('[RiffleBridge] Core initialized');
68
+ }
69
+
70
+ /**
71
+ * 检查是否在 React Native WebView 环境中
72
+ */
73
+ isAvailable(): boolean {
74
+ return typeof window !== 'undefined' &&
75
+ typeof window.ReactNativeWebView !== 'undefined';
76
+ }
77
+
78
+ /**
79
+ * 等待 Bridge 就绪
80
+ */
81
+ async waitForReady(timeout = 3000): Promise<boolean> {
82
+ if (this.isAvailable()) return true;
83
+
84
+ return new Promise((resolve) => {
85
+ const startTime = Date.now();
86
+
87
+ const check = () => {
88
+ if (this.isAvailable()) {
89
+ resolve(true);
90
+ return;
91
+ }
92
+
93
+ if (Date.now() - startTime > timeout) {
94
+ resolve(false);
95
+ return;
96
+ }
97
+
98
+ setTimeout(check, 50);
99
+ };
100
+
101
+ check();
102
+ });
103
+ }
104
+
105
+ /**
106
+ * 确保在 React Native WebView 环境中运行
107
+ * @throws Error 如果不在 RN WebView 环境中
108
+ */
109
+ private ensureAvailable(): void {
110
+ if (!this.isAvailable()) {
111
+ throw new Error(
112
+ '[RiffleBridge] Not available. This SDK requires React Native WebView environment.\n' +
113
+ 'Please ensure:\n' +
114
+ '1. Your app is running inside a React Native WebView\n' +
115
+ '2. Native side has injected the minimal bridge script\n' +
116
+ '3. window.ReactNativeWebView is available'
117
+ );
118
+ }
119
+ }
120
+
121
+ /**
122
+ * 发送消息到 Native
123
+ * @throws Error 如果不在 RN WebView 环境中
124
+ */
125
+ async send<T = unknown>(
126
+ module: string,
127
+ method: string,
128
+ params: Record<string, unknown> = {}
129
+ ): Promise<T> {
130
+ this.ensureAvailable();
131
+
132
+ const id = `msg_${++this.messageId}_${Date.now()}`;
133
+ const timeout = getTimeout(module, method);
134
+
135
+ return new Promise((resolve, reject) => {
136
+ // 设置回调
137
+ window.__RIFFLE_CALLBACKS__!.set(id, (response: BridgeResponse) => {
138
+ if (response.success) {
139
+ resolve(response.data as T);
140
+ } else {
141
+ reject(new Error(response.error || 'Unknown error'));
142
+ }
143
+ });
144
+
145
+ // 发送消息
146
+ const message = {
147
+ id,
148
+ module,
149
+ method,
150
+ params,
151
+ timestamp: Date.now(),
152
+ };
153
+
154
+ window.ReactNativeWebView!.postMessage(JSON.stringify(message));
155
+
156
+ // 设置超时
157
+ setTimeout(() => {
158
+ if (window.__RIFFLE_CALLBACKS__!.has(id)) {
159
+ window.__RIFFLE_CALLBACKS__!.delete(id);
160
+ reject(new Error(`Request timeout (${timeout / 1000}s) for ${module}.${method}`));
161
+ }
162
+ }, timeout);
163
+ });
164
+ }
165
+
166
+ /**
167
+ * 处理 Native 响应
168
+ */
169
+ private handleResponse(responseStr: string): void {
170
+ try {
171
+ const response: BridgeResponse = JSON.parse(responseStr);
172
+ const callback = window.__RIFFLE_CALLBACKS__?.get(response.id);
173
+
174
+ if (callback) {
175
+ window.__RIFFLE_CALLBACKS__!.delete(response.id);
176
+ callback(response);
177
+ }
178
+ } catch (error) {
179
+ console.error('[RiffleBridge] Failed to handle response:', error);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * 处理传感器数据
185
+ */
186
+ private handleSensorData(data: { sensor: string; values: unknown[] }): void {
187
+ const { sensor, values } = data;
188
+ const handlers = this.sensorHandlers.get(sensor);
189
+
190
+ if (handlers) {
191
+ handlers.forEach((handler) => {
192
+ try {
193
+ handler(values);
194
+ } catch (error) {
195
+ console.error(`[RiffleBridge] Sensor callback error (${sensor}):`, error);
196
+ }
197
+ });
198
+ }
199
+ }
200
+
201
+ /**
202
+ * 订阅传感器数据
203
+ */
204
+ onSensorData(sensor: string, callback: (data: unknown[]) => void): () => void {
205
+ if (!this.sensorHandlers.has(sensor)) {
206
+ this.sensorHandlers.set(sensor, new Set());
207
+ }
208
+
209
+ this.sensorHandlers.get(sensor)!.add(callback);
210
+
211
+ // 返回取消订阅函数
212
+ return () => {
213
+ this.sensorHandlers.get(sensor)?.delete(callback);
214
+ };
215
+ }
216
+
217
+ /**
218
+ * 清除所有传感器订阅
219
+ */
220
+ clearSensorSubscriptions(): void {
221
+ this.sensorHandlers.clear();
222
+ }
223
+ }
224
+
225
+ // 单例实例
226
+ let bridgeCoreInstance: BridgeCore | null = null;
227
+
228
+ /**
229
+ * 获取 BridgeCore 单例
230
+ */
231
+ export function getBridgeCore(): BridgeCore {
232
+ if (!bridgeCoreInstance) {
233
+ bridgeCoreInstance = new BridgeCore();
234
+ }
235
+ return bridgeCoreInstance;
236
+ }
package/src/index.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @ayden-fc2/riffle-bridge-web
3
+ *
4
+ * Riffle Bridge Web SDK - WebView 与 Native 通信桥接库
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { RiffleBridge, createTweaks } from '@ayden-fc2/riffle-bridge-web';
9
+ *
10
+ * // 基础使用
11
+ * const bridge = new RiffleBridge();
12
+ * await bridge.vibrate('success');
13
+ *
14
+ * // Tweaks 配置
15
+ * const tweaks = createTweaks({
16
+ * color: { name: '颜色', type: 'color', value: '#667eea' },
17
+ * }, { React });
18
+ *
19
+ * const color = tweaks.color.useState();
20
+ * ```
21
+ */
22
+
23
+ // 核心类
24
+ export { BridgeCore, getBridgeCore } from './core';
25
+ export { RiffleBridge, getRiffleBridge, Utils } from './bridge';
26
+
27
+ // 控制器
28
+ export {
29
+ HapticController,
30
+ SensorController,
31
+ DeviceController,
32
+ FileStorageController,
33
+ AudioController,
34
+ CameraController,
35
+ MicrophoneController,
36
+ ConfigController,
37
+ } from './controllers';
38
+
39
+ // Tweaks 配置系统
40
+ export {
41
+ TweakValue,
42
+ RiffleBridgeTweaks,
43
+ createTweaks,
44
+ riffleBridgeTweaks,
45
+ } from './tweaks';
46
+
47
+ // 类型导出
48
+ export type {
49
+ // 基础类型
50
+ HapticFeedbackType,
51
+ SensorType,
52
+ CameraFacing,
53
+ CameraFilter,
54
+ TiltDirection,
55
+ ShakeIntensity,
56
+
57
+ // 数据接口
58
+ SensorData,
59
+ BarometerData,
60
+ VolumeData,
61
+ AudioStatus,
62
+ DeviceInfo,
63
+ BatteryInfo,
64
+ NetworkInfo,
65
+ SystemInfo,
66
+ AppInfo,
67
+ ScreenInfo,
68
+ StorageStats,
69
+ FileInfo,
70
+ CachedFileInfo,
71
+ DownloadResult,
72
+ Base64Result,
73
+ PhotoResult,
74
+ RecordingResult,
75
+ PermissionResult,
76
+
77
+ // Tweaks 类型
78
+ TweakType,
79
+ TweakConfig,
80
+ TweaksConfig,
81
+ SelectOption,
82
+ ColorTweakConfig,
83
+ NumberTweakConfig,
84
+ BooleanTweakConfig,
85
+ StringTweakConfig,
86
+ SelectTweakConfig,
87
+
88
+ // Bridge 消息类型
89
+ BridgeMessage,
90
+ BridgeResponse,
91
+ } from './types';
92
+
93
+ // 控制器选项类型
94
+ export type { SensorStartOptions, PlayOptions } from './controllers';
package/src/tweaks.ts ADDED
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Riffle Bridge Tweaks 配置系统
3
+ *
4
+ * 提供响应式配置管理,支持与 Native 端双向同步
5
+ */
6
+
7
+ import type { TweakConfig, TweaksConfig } from './types';
8
+ import { getRiffleBridge } from './bridge';
9
+
10
+ type ReactType = typeof import('react');
11
+
12
+ /**
13
+ * 单个配置项的值包装器
14
+ */
15
+ export class TweakValue<T> {
16
+ private _value: T;
17
+ private _listeners: Set<() => void> = new Set();
18
+ private _syncCallback: (() => void) | null;
19
+
20
+ constructor(
21
+ private _key: string,
22
+ private _config: TweakConfig,
23
+ syncCallback: (() => void) | null = null
24
+ ) {
25
+ this._value = _config.value as T;
26
+ this._syncCallback = syncCallback;
27
+ }
28
+
29
+ /** 获取当前值 */
30
+ get value(): T {
31
+ return this._value;
32
+ }
33
+
34
+ /** 获取配置 */
35
+ get config(): TweakConfig {
36
+ return this._config;
37
+ }
38
+
39
+ /** 获取键名 */
40
+ get key(): string {
41
+ return this._key;
42
+ }
43
+
44
+ /**
45
+ * 设置值
46
+ * @param newValue 新值
47
+ * @param skipSync 是否跳过同步到 Native
48
+ */
49
+ set(newValue: T, skipSync = false): void {
50
+ if (this._value !== newValue) {
51
+ this._value = newValue;
52
+ this._notifyListeners();
53
+ if (!skipSync && this._syncCallback) {
54
+ this._syncCallback();
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 内部设置(不触发同步,用于接收 Native 更新)
61
+ */
62
+ _setInternal(newValue: T): void {
63
+ if (this._value !== newValue) {
64
+ this._value = newValue;
65
+ this._notifyListeners();
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 订阅值变化
71
+ */
72
+ subscribe(listener: () => void): () => void {
73
+ this._listeners.add(listener);
74
+ return () => this._listeners.delete(listener);
75
+ }
76
+
77
+ /**
78
+ * 监听值变化
79
+ */
80
+ onChange(callback: (value: T) => void): () => void {
81
+ return this.subscribe(() => callback(this._value));
82
+ }
83
+
84
+ /**
85
+ * React Hook: 获取响应式值
86
+ * 需要传入 React 实例或使用 createTweaks 时传入
87
+ */
88
+ useState(react?: ReactType): T {
89
+ const ReactRef = react || _globalReact;
90
+
91
+ if (!ReactRef?.useSyncExternalStore) {
92
+ console.warn('[RiffleBridgeTweaks] React.useSyncExternalStore not available, returning static value');
93
+ return this._value;
94
+ }
95
+
96
+ return ReactRef.useSyncExternalStore(
97
+ (callback) => this.subscribe(callback),
98
+ () => this._value,
99
+ () => this._value
100
+ );
101
+ }
102
+
103
+ private _notifyListeners(): void {
104
+ this._listeners.forEach((listener) => {
105
+ try {
106
+ listener();
107
+ } catch (e) {
108
+ console.error('[RiffleBridgeTweaks] Listener error:', e);
109
+ }
110
+ });
111
+ }
112
+ }
113
+
114
+ // 全局 React 引用
115
+ let _globalReact: ReactType | null = null;
116
+
117
+ /**
118
+ * Tweaks 配置管理器
119
+ */
120
+ export class RiffleBridgeTweaks<T extends TweaksConfig> {
121
+ private _config: T;
122
+ private _tweaks: Map<string, TweakValue<unknown>> = new Map();
123
+ private _syncDebounceTimer: ReturnType<typeof setTimeout> | null = null;
124
+
125
+ constructor(config: T) {
126
+ this._config = config;
127
+
128
+ // 创建防抖同步函数
129
+ const debouncedSync = () => {
130
+ if (this._syncDebounceTimer) {
131
+ clearTimeout(this._syncDebounceTimer);
132
+ }
133
+ this._syncDebounceTimer = setTimeout(() => {
134
+ this._syncToNative();
135
+ }, 50);
136
+ };
137
+
138
+ // 初始化各配置项
139
+ for (const [key, cfg] of Object.entries(config)) {
140
+ this._tweaks.set(key, new TweakValue(key, cfg, debouncedSync));
141
+ }
142
+
143
+ // 设置消息监听和全局回调
144
+ this._setupMessageListener();
145
+ this._setupGlobalCallbacks();
146
+
147
+ // 初始同步到 Native
148
+ this._syncToNative();
149
+
150
+ console.log('[RiffleBridgeTweaks] Initialized');
151
+ }
152
+
153
+ /**
154
+ * 获取指定键的配置项
155
+ */
156
+ get<K extends keyof T>(key: K): TweakValue<T[K]['value']> {
157
+ return this._tweaks.get(key as string) as TweakValue<T[K]['value']>;
158
+ }
159
+
160
+ /**
161
+ * 获取所有值
162
+ */
163
+ getData(): Record<string, unknown> {
164
+ const data: Record<string, unknown> = {};
165
+ for (const [key, tweak] of this._tweaks) {
166
+ data[key] = tweak.value;
167
+ }
168
+ return data;
169
+ }
170
+
171
+ /**
172
+ * 获取完整配置(含元数据)
173
+ */
174
+ toJSON(): Record<string, { name: string; type: string; value: unknown; [k: string]: unknown }> {
175
+ const result: Record<string, { name: string; type: string; value: unknown; [k: string]: unknown }> = {};
176
+ for (const [key, tweak] of this._tweaks) {
177
+ result[key] = { ...tweak.config, value: tweak.value };
178
+ }
179
+ return result;
180
+ }
181
+
182
+ /**
183
+ * 批量更新值
184
+ */
185
+ update(updates: Partial<Record<keyof T, unknown>>): void {
186
+ for (const [key, value] of Object.entries(updates)) {
187
+ const tweak = this._tweaks.get(key);
188
+ if (tweak) {
189
+ tweak.set(value);
190
+ }
191
+ }
192
+ this._syncToNative();
193
+ }
194
+
195
+ /**
196
+ * 重置为默认值
197
+ */
198
+ reset(): void {
199
+ for (const [key, tweak] of this._tweaks) {
200
+ tweak.set(this._config[key].value);
201
+ }
202
+ this._syncToNative();
203
+ }
204
+
205
+ /**
206
+ * 获取所有配置键名
207
+ */
208
+ keys(): string[] {
209
+ return Array.from(this._tweaks.keys());
210
+ }
211
+
212
+ /**
213
+ * 监听任意参数变化
214
+ */
215
+ onAnyChange(callback: (key: string, value: unknown) => void): () => void {
216
+ const unsubscribes: Array<() => void> = [];
217
+ for (const [key, tweak] of this._tweaks) {
218
+ unsubscribes.push(tweak.onChange((value) => callback(key, value)));
219
+ }
220
+ return () => unsubscribes.forEach((unsub) => unsub());
221
+ }
222
+
223
+ /**
224
+ * 设置消息监听
225
+ */
226
+ private _setupMessageListener(): void {
227
+ if (typeof window === 'undefined') return;
228
+
229
+ window.addEventListener('message', (event) => {
230
+ const data = event.data;
231
+ if (!data || typeof data !== 'object') return;
232
+
233
+ const { type, payload } = data as { type?: string; payload?: Record<string, unknown> };
234
+
235
+ // 单个值更新
236
+ if (type === 'tweak-update' && payload) {
237
+ const { key, value } = payload as { key: string; value: unknown };
238
+ const tweak = this._tweaks.get(key);
239
+ if (tweak) {
240
+ tweak._setInternal(value);
241
+ }
242
+ }
243
+
244
+ // 批量更新
245
+ if (type === 'tweaks-update' && payload) {
246
+ for (const [key, value] of Object.entries(payload)) {
247
+ const tweak = this._tweaks.get(key);
248
+ if (tweak) {
249
+ tweak._setInternal(value);
250
+ }
251
+ }
252
+ }
253
+ });
254
+ }
255
+
256
+ /**
257
+ * 设置全局回调函数(兼容旧版 Native 调用方式)
258
+ */
259
+ private _setupGlobalCallbacks(): void {
260
+ if (typeof window === 'undefined') return;
261
+
262
+ const self = this;
263
+
264
+ // 单个值更新回调
265
+ (window as unknown as Record<string, unknown>).updateConfigFromNative = function (
266
+ key: string,
267
+ value: unknown
268
+ ) {
269
+ const tweak = self._tweaks.get(key);
270
+ if (tweak) {
271
+ tweak._setInternal(value);
272
+ }
273
+ };
274
+
275
+ // 批量更新回调
276
+ (window as unknown as Record<string, unknown>).applyConfigFromNative = function (
277
+ allData: Record<string, unknown>
278
+ ) {
279
+ for (const [key, value] of Object.entries(allData)) {
280
+ const tweak = self._tweaks.get(key);
281
+ if (tweak) {
282
+ tweak._setInternal(value);
283
+ }
284
+ }
285
+ };
286
+
287
+ // 重置回调
288
+ (window as unknown as Record<string, unknown>).resetConfigToDefaults = function () {
289
+ for (const [key, tweak] of self._tweaks) {
290
+ tweak._setInternal(self._config[key].value);
291
+ }
292
+ };
293
+ }
294
+
295
+ /**
296
+ * 同步配置到 Native
297
+ */
298
+ private _syncToNative(): void {
299
+ try {
300
+ const bridge = getRiffleBridge();
301
+ if (!bridge.isAvailable()) {
302
+ return;
303
+ }
304
+
305
+ bridge.config.updateParams({
306
+ tweaks: this.toJSON(),
307
+ data: this.getData(),
308
+ timestamp: Date.now(),
309
+ });
310
+ } catch (e) {
311
+ console.error('[RiffleBridgeTweaks] Sync failed:', e);
312
+ }
313
+ }
314
+ }
315
+
316
+ // Tweaks 实例的代理类型
317
+ type TweaksProxy<T extends TweaksConfig> = RiffleBridgeTweaks<T> & {
318
+ [K in keyof T]: TweakValue<T[K]['value']>;
319
+ };
320
+
321
+ /**
322
+ * 创建 Tweaks 配置实例
323
+ *
324
+ * @example
325
+ * ```typescript
326
+ * import React from 'react';
327
+ * import { createTweaks } from '@riffle/bridge-web';
328
+ *
329
+ * const tweaks = createTweaks({
330
+ * color: { name: '颜色', type: 'color', value: '#667eea' },
331
+ * size: { name: '大小', type: 'number', value: 150, min: 50, max: 300 },
332
+ * enabled: { name: '启用', type: 'boolean', value: true },
333
+ * }, { React });
334
+ *
335
+ * function Demo() {
336
+ * const color = tweaks.color.useState();
337
+ * const size = tweaks.size.useState();
338
+ *
339
+ * return <div style={{ backgroundColor: color, width: size, height: size }} />;
340
+ * }
341
+ * ```
342
+ */
343
+ export function createTweaks<T extends TweaksConfig>(
344
+ config: T,
345
+ options: { React?: ReactType } = {}
346
+ ): TweaksProxy<T> {
347
+ // 保存 React 引用
348
+ if (options.React) {
349
+ _globalReact = options.React;
350
+ }
351
+
352
+ const instance = new RiffleBridgeTweaks(config);
353
+
354
+ // 创建代理,支持直接访问配置项
355
+ return new Proxy(instance, {
356
+ get(target, prop) {
357
+ // 优先返回实例方法
358
+ if (prop in target) {
359
+ const value = target[prop as keyof typeof target];
360
+ return typeof value === 'function' ? value.bind(target) : value;
361
+ }
362
+ // 否则返回配置项
363
+ return target.get(prop as keyof T);
364
+ },
365
+ ownKeys(target) {
366
+ return target.keys();
367
+ },
368
+ getOwnPropertyDescriptor(target, prop) {
369
+ if (target.keys().includes(prop as string)) {
370
+ return { enumerable: true, configurable: true };
371
+ }
372
+ return undefined;
373
+ },
374
+ }) as TweaksProxy<T>;
375
+ }
376
+
377
+ // 兼容全局函数调用方式
378
+ export function riffleBridgeTweaks<T extends TweaksConfig>(
379
+ config: T,
380
+ options: { React?: ReactType } = {}
381
+ ): TweaksProxy<T> {
382
+ return createTweaks(config, options);
383
+ }