react-native-debug-toolkit 2.3.0 → 3.1.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 +115 -97
- package/README.zh-CN.md +113 -95
- package/bin/debug-toolkit.js +114 -0
- package/lib/commonjs/core/initialize.js +5 -0
- package/lib/commonjs/core/initialize.js.map +1 -1
- package/lib/commonjs/features/network/index.js +28 -2
- package/lib/commonjs/features/network/index.js.map +1 -1
- package/lib/commonjs/features/network/networkInterceptor.js +14 -6
- package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
- package/lib/commonjs/index.js +56 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/panel/DebugPanel.js +25 -0
- package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
- package/lib/commonjs/ui/panel/FloatPanelView.js +15 -62
- package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js +495 -0
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js +71 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/commonjs/utils/DaemonClient.js +721 -0
- package/lib/commonjs/utils/DaemonClient.js.map +1 -0
- package/lib/commonjs/utils/createPersistedObservableStore.js +23 -3
- package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/commonjs/utils/deviceReport.js +132 -0
- package/lib/commonjs/utils/deviceReport.js.map +1 -0
- package/lib/module/core/initialize.js +6 -0
- package/lib/module/core/initialize.js.map +1 -1
- package/lib/module/features/network/index.js +25 -1
- package/lib/module/features/network/index.js.map +1 -1
- package/lib/module/features/network/networkInterceptor.js +14 -6
- package/lib/module/features/network/networkInterceptor.js.map +1 -1
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/panel/DebugPanel.js +26 -1
- package/lib/module/ui/panel/DebugPanel.js.map +1 -1
- package/lib/module/ui/panel/FloatPanelView.js +16 -63
- package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/module/ui/panel/StreamingSettingsModal.js +490 -0
- package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/module/ui/panel/useTabAnimation.js +67 -0
- package/lib/module/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/module/utils/DaemonClient.js +703 -0
- package/lib/module/utils/DaemonClient.js.map +1 -0
- package/lib/module/utils/createPersistedObservableStore.js +23 -3
- package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/module/utils/deviceReport.js +128 -0
- package/lib/module/utils/deviceReport.js.map +1 -0
- package/lib/typescript/src/core/initialize.d.ts.map +1 -1
- package/lib/typescript/src/features/network/index.d.ts +2 -0
- package/lib/typescript/src/features/network/index.d.ts.map +1 -1
- package/lib/typescript/src/features/network/networkInterceptor.d.ts +1 -1
- package/lib/typescript/src/features/network/networkInterceptor.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +8 -0
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts +14 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts.map +1 -0
- package/lib/typescript/src/utils/DaemonClient.d.ts +141 -0
- package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -0
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +2 -1
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
- package/lib/typescript/src/utils/deviceReport.d.ts +18 -0
- package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -0
- package/node/daemon/src/cli.js +82 -0
- package/node/daemon/src/console/console.html +1662 -0
- package/node/daemon/src/console/index.js +47 -0
- package/node/daemon/src/constants.js +38 -0
- package/node/daemon/src/index.js +11 -0
- package/node/daemon/src/server.js +447 -0
- package/node/daemon/src/store.js +187 -0
- package/node/mcp/src/cli.js +31 -0
- package/node/mcp/src/constants.js +13 -0
- package/node/mcp/src/daemonClient.js +132 -0
- package/node/mcp/src/httpClient.js +49 -0
- package/node/mcp/src/index.js +15 -0
- package/node/mcp/src/logs.js +96 -0
- package/node/mcp/src/server.js +144 -0
- package/node/mcp/src/tools.js +84 -0
- package/package.json +8 -3
- package/src/core/initialize.ts +8 -0
- package/src/features/network/index.ts +30 -3
- package/src/features/network/networkInterceptor.ts +19 -6
- package/src/index.ts +22 -0
- package/src/ui/panel/DebugPanel.tsx +23 -1
- package/src/ui/panel/FloatPanelView.tsx +10 -68
- package/src/ui/panel/StreamingSettingsModal.tsx +528 -0
- package/src/ui/panel/useTabAnimation.ts +77 -0
- package/src/utils/DaemonClient.ts +887 -0
- package/src/utils/createPersistedObservableStore.ts +16 -3
- package/src/utils/deviceReport.ts +203 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getDaemonOrigin, readDevice, readDevices } = require('./daemonClient');
|
|
4
|
+
const { KNOWN_LOG_TYPES, createToolPayload } = require('./logs');
|
|
5
|
+
|
|
6
|
+
const getAppLogsTool = {
|
|
7
|
+
name: 'get_app_logs',
|
|
8
|
+
description: 'Read React Native Debug Toolkit logs from the local daemon. Tip: if you have shell access, curl http://127.0.0.1:3799/devices/latest is more efficient.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
deviceId: { type: 'string' },
|
|
13
|
+
logType: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
enum: KNOWN_LOG_TYPES,
|
|
16
|
+
},
|
|
17
|
+
limit: { type: 'number', default: 50 },
|
|
18
|
+
failedOnly: { type: 'boolean', default: false },
|
|
19
|
+
includeBodies: { type: 'boolean', default: true },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const listAppDevicesTool = {
|
|
25
|
+
name: 'list_app_devices',
|
|
26
|
+
description: 'List React Native Debug Toolkit devices available in the local daemon. Tip: if you have shell access, curl http://127.0.0.1:3799/devices is more efficient.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const tools = [getAppLogsTool, listAppDevicesTool];
|
|
34
|
+
|
|
35
|
+
async function callTool(name, args = {}, context = {}) {
|
|
36
|
+
const ensureDaemon = context.ensureDaemon || (async () => ({ ok: true, origin: getDaemonOrigin() }));
|
|
37
|
+
const daemon = await ensureDaemon();
|
|
38
|
+
if (!daemon.ok) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: daemon.error || 'Debug toolkit daemon is not available',
|
|
42
|
+
origin: daemon.origin,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (name === listAppDevicesTool.name) {
|
|
48
|
+
const readDevicesImpl = context.readDevices || readDevices;
|
|
49
|
+
const result = await readDevicesImpl(daemon.origin);
|
|
50
|
+
const devices = Array.isArray(result.devices) ? result.devices : [];
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
origin: daemon.origin,
|
|
54
|
+
devices,
|
|
55
|
+
count: devices.length,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (name !== getAppLogsTool.name) {
|
|
60
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const device = await readDevice(daemon.origin, args.deviceId);
|
|
64
|
+
return createToolPayload(device, {
|
|
65
|
+
logType: args.logType,
|
|
66
|
+
limit: args.limit,
|
|
67
|
+
failedOnly: args.failedOnly,
|
|
68
|
+
includeBodies: args.includeBodies,
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: error.message || 'Failed to read debug toolkit logs',
|
|
74
|
+
origin: daemon.origin,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
callTool,
|
|
81
|
+
getAppLogsTool,
|
|
82
|
+
listAppDevicesTool,
|
|
83
|
+
tools,
|
|
84
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-debug-toolkit",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "3.1.2",
|
|
4
|
+
"description": "A local-first React Native debugging bridge with in-app logs, desktop daemon, Web Console, HTTP API, and MCP support",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
7
7
|
"types": "lib/typescript/src/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
9
|
"src",
|
|
10
10
|
"lib",
|
|
11
|
+
"bin",
|
|
12
|
+
"node",
|
|
11
13
|
"README.md",
|
|
12
14
|
"LICENSE",
|
|
13
15
|
"!**/__tests__",
|
|
@@ -19,9 +21,12 @@
|
|
|
19
21
|
"typescript": "npm run typecheck",
|
|
20
22
|
"test": "jest",
|
|
21
23
|
"build": "bob build",
|
|
22
|
-
"lint": "eslint \"src/**/*.{js,ts,tsx}\"",
|
|
24
|
+
"lint": "eslint \"src/**/*.{js,ts,tsx}\" \"node/**/*.js\" \"bin/**/*.js\"",
|
|
23
25
|
"prepare": "bob build"
|
|
24
26
|
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"debug-toolkit": "bin/debug-toolkit.js"
|
|
29
|
+
},
|
|
25
30
|
"keywords": [
|
|
26
31
|
"react-native",
|
|
27
32
|
"debug",
|
package/src/core/initialize.ts
CHANGED
|
@@ -11,10 +11,16 @@ import { createTrackFeature } from '../features/track';
|
|
|
11
11
|
import type { TrackFeatureConfig } from '../features/track';
|
|
12
12
|
import { createEnvironmentFeature } from '../features/environment';
|
|
13
13
|
import { createClipboardFeature } from '../features/clipboard';
|
|
14
|
+
import { daemonClient, restoreDaemonStreaming } from '../utils/DaemonClient';
|
|
15
|
+
import { _addDaemonEndpointToNetworkBlacklist } from '../features/network';
|
|
14
16
|
import type { AnyDebugFeature, BuiltInFeatureName } from '../types';
|
|
15
17
|
|
|
16
18
|
const isDebugMode = __DEV__;
|
|
17
19
|
|
|
20
|
+
daemonClient.setEndpointDetector((url) => {
|
|
21
|
+
_addDaemonEndpointToNetworkBlacklist(url);
|
|
22
|
+
});
|
|
23
|
+
|
|
18
24
|
/** Feature-specific configuration map */
|
|
19
25
|
export interface FeatureConfigs {
|
|
20
26
|
network?: boolean | NetworkFeatureConfig;
|
|
@@ -114,6 +120,8 @@ export function initializeDebugToolkit(
|
|
|
114
120
|
DebugToolkit.hideLauncher();
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
restoreDaemonStreaming().catch(() => {});
|
|
124
|
+
|
|
117
125
|
return DebugToolkit;
|
|
118
126
|
} catch (error) {
|
|
119
127
|
console.error('[DebugToolkit] Initialization failed:', error);
|
|
@@ -9,8 +9,7 @@ import {
|
|
|
9
9
|
startXMLHttpRequest,
|
|
10
10
|
resetInterceptors,
|
|
11
11
|
} from './networkInterceptor';
|
|
12
|
-
|
|
13
|
-
type NetworkLogPayload = Omit<NetworkLogEntry, 'id'>;
|
|
12
|
+
import type { NetworkLogPayload } from './networkInterceptor';
|
|
14
13
|
|
|
15
14
|
// ─── Utilities ────────────────────────────────────────
|
|
16
15
|
|
|
@@ -37,6 +36,7 @@ function emitNetworkLog(entry: NetworkLogPayload): void {
|
|
|
37
36
|
// ─── Feature factory ──────────────────────────────────
|
|
38
37
|
|
|
39
38
|
const DEFAULT_MAX_LOGS = 200;
|
|
39
|
+
const daemonEndpointBlacklist: Array<string | RegExp> = [];
|
|
40
40
|
|
|
41
41
|
export interface NetworkFeatureConfig {
|
|
42
42
|
/** Maximum number of network logs to keep (default: 200) */
|
|
@@ -57,7 +57,7 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
|
|
|
57
57
|
let stopXhrFn: (() => void) | null = null;
|
|
58
58
|
|
|
59
59
|
const handleLog = (entry: NetworkLogPayload) => {
|
|
60
|
-
if (isUrlBlacklisted(entry.request.url, blacklist)) {
|
|
60
|
+
if (isUrlBlacklisted(entry.request.url, [...blacklist, ...daemonEndpointBlacklist])) {
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
logStore.push({ ...entry, id: logStore.nextId() }, maxLogs);
|
|
@@ -95,8 +95,35 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
|
|
|
95
95
|
};
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
+
function normalizeDaemonEndpoint(endpoint: string): string {
|
|
99
|
+
const trimmed = endpoint.trim().replace(/\/+$/, '');
|
|
100
|
+
if (!trimmed) {
|
|
101
|
+
return trimmed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const url = new URL(trimmed);
|
|
106
|
+
return `${url.origin}${url.pathname === '/' ? '' : url.pathname}`;
|
|
107
|
+
} catch {
|
|
108
|
+
return trimmed;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function _addDaemonEndpointToNetworkBlacklist(endpoint: string): void {
|
|
113
|
+
const normalized = normalizeDaemonEndpoint(endpoint);
|
|
114
|
+
if (!normalized || daemonEndpointBlacklist.includes(normalized)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
daemonEndpointBlacklist.push(normalized);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function _isNetworkUrlBlacklistedForTesting(url: string): boolean {
|
|
121
|
+
return isUrlBlacklisted(url, daemonEndpointBlacklist);
|
|
122
|
+
}
|
|
123
|
+
|
|
98
124
|
/** Reset module-level state for testing */
|
|
99
125
|
export function _resetNetworkForTesting(): void {
|
|
100
126
|
networkChannel = createEventChannel<NetworkLogPayload>();
|
|
127
|
+
daemonEndpointBlacklist.splice(0, daemonEndpointBlacklist.length);
|
|
101
128
|
resetInterceptors();
|
|
102
129
|
}
|
|
@@ -3,6 +3,8 @@ import { urlRewriter } from '../../utils/urlRewriterRegistry';
|
|
|
3
3
|
|
|
4
4
|
type NetworkLogPayload = Omit<NetworkLogEntry, 'id'>;
|
|
5
5
|
|
|
6
|
+
export type { NetworkLogPayload };
|
|
7
|
+
|
|
6
8
|
// Intercepts React Native's XMLHttpRequest transport layer.
|
|
7
9
|
// RN fetch and axios (default adapter) both go through XHR — one hook captures everything.
|
|
8
10
|
|
|
@@ -113,6 +115,15 @@ function safeRead<T>(read: () => T): T | undefined {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
// URLs matching these patterns are skipped (Metro dev server internals).
|
|
119
|
+
const IGNORED_URL_PATTERNS = [
|
|
120
|
+
/\/symbolicate$/,
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
function shouldIgnoreUrl(url: string): boolean {
|
|
124
|
+
return IGNORED_URL_PATTERNS.some((p) => p.test(url));
|
|
125
|
+
}
|
|
126
|
+
|
|
116
127
|
function getGlobalXMLHttpRequest(): XMLHttpRequestConstructorLike | undefined {
|
|
117
128
|
return (globalThis as { XMLHttpRequest?: XMLHttpRequestConstructorLike }).XMLHttpRequest;
|
|
118
129
|
}
|
|
@@ -183,6 +194,9 @@ export function startXMLHttpRequest(
|
|
|
183
194
|
...args: unknown[]
|
|
184
195
|
) {
|
|
185
196
|
const rewrittenUrl = urlRewriter.get() ? rewriteUrl(url) : url;
|
|
197
|
+
if (shouldIgnoreUrl(rewrittenUrl)) {
|
|
198
|
+
return originalXhrOpen!.call(this, method, rewrittenUrl, ...args);
|
|
199
|
+
}
|
|
186
200
|
pendingXhrRequests.set(this, {
|
|
187
201
|
method: (method || 'GET').toUpperCase(),
|
|
188
202
|
url: rewrittenUrl,
|
|
@@ -209,12 +223,11 @@ export function startXMLHttpRequest(
|
|
|
209
223
|
body?: unknown,
|
|
210
224
|
) {
|
|
211
225
|
const that = this;
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
};
|
|
226
|
+
const existingState = pendingXhrRequests.get(that);
|
|
227
|
+
if (!existingState) {
|
|
228
|
+
return originalXhrSend!.call(that, body);
|
|
229
|
+
}
|
|
230
|
+
const state = existingState;
|
|
218
231
|
state.body = body;
|
|
219
232
|
state.timestamp = Date.now();
|
|
220
233
|
pendingXhrRequests.set(that, state);
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,28 @@ export { useNavigationLogger } from './features/navigation/useNavigationLogger';
|
|
|
28
28
|
export { safeStringify } from './utils/safeStringify';
|
|
29
29
|
export { copyToComputer, logToComputer, fmt } from './utils/copyToComputer';
|
|
30
30
|
export type { CopyResult, CopyOptions, CopyMethod } from './utils/copyToComputer';
|
|
31
|
+
export { createDebugDeviceReport } from './utils/deviceReport';
|
|
32
|
+
export type { DebugDeviceReport, DebugDeviceReportOptions } from './utils/deviceReport';
|
|
33
|
+
export { DaemonClient, daemonClient } from './utils/DaemonClient';
|
|
34
|
+
export type {
|
|
35
|
+
DaemonSettings,
|
|
36
|
+
DaemonConnectionMode,
|
|
37
|
+
DaemonConnectionFailureReason,
|
|
38
|
+
DaemonConnectionOptions,
|
|
39
|
+
DaemonConnectionResult,
|
|
40
|
+
StreamStatus,
|
|
41
|
+
StreamToDaemonOptions,
|
|
42
|
+
ReportResult,
|
|
43
|
+
ReportToDaemonOptions,
|
|
44
|
+
} from './utils/DaemonClient';
|
|
45
|
+
export {
|
|
46
|
+
getDefaultDaemonEndpoint,
|
|
47
|
+
reportDebugDeviceToDaemon,
|
|
48
|
+
checkDaemonConnection,
|
|
49
|
+
startStreaming,
|
|
50
|
+
stopStreaming,
|
|
51
|
+
isStreaming,
|
|
52
|
+
} from './utils/DaemonClient';
|
|
31
53
|
|
|
32
54
|
// Types
|
|
33
55
|
export type {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
useWindowDimensions,
|
|
11
11
|
} from 'react-native';
|
|
12
12
|
import { Colors } from '../theme/colors';
|
|
13
|
+
import { StreamingSettingsModal } from './StreamingSettingsModal';
|
|
13
14
|
|
|
14
15
|
interface DebugPanelProps {
|
|
15
16
|
onClose: () => void;
|
|
@@ -21,6 +22,7 @@ export function DebugPanel({ onClose, onClearAll, children }: DebugPanelProps) {
|
|
|
21
22
|
const { height: screenHeight } = useWindowDimensions();
|
|
22
23
|
const panelTranslateY = useRef(new Animated.Value(screenHeight)).current;
|
|
23
24
|
const backdropOpacity = useRef(new Animated.Value(0)).current;
|
|
25
|
+
const [settingsVisible, setSettingsVisible] = useState(false);
|
|
24
26
|
|
|
25
27
|
useEffect(() => {
|
|
26
28
|
requestAnimationFrame(() => {
|
|
@@ -101,6 +103,13 @@ export function DebugPanel({ onClose, onClearAll, children }: DebugPanelProps) {
|
|
|
101
103
|
<View style={styles.header}>
|
|
102
104
|
<Text style={styles.headerTitle}>Debug Toolkit</Text>
|
|
103
105
|
<View style={styles.headerButtons}>
|
|
106
|
+
<TouchableOpacity
|
|
107
|
+
onPress={() => setSettingsVisible(true)}
|
|
108
|
+
style={styles.settingsButton}
|
|
109
|
+
activeOpacity={0.6}
|
|
110
|
+
>
|
|
111
|
+
<Text style={styles.settingsButtonText}>⚙</Text>
|
|
112
|
+
</TouchableOpacity>
|
|
104
113
|
<TouchableOpacity
|
|
105
114
|
onPress={() => {
|
|
106
115
|
onClearAll();
|
|
@@ -119,6 +128,7 @@ export function DebugPanel({ onClose, onClearAll, children }: DebugPanelProps) {
|
|
|
119
128
|
</View>
|
|
120
129
|
<View style={styles.panelContent}>{children}</View>
|
|
121
130
|
</Animated.View>
|
|
131
|
+
<StreamingSettingsModal visible={settingsVisible} onClose={() => setSettingsVisible(false)} />
|
|
122
132
|
</View>
|
|
123
133
|
);
|
|
124
134
|
}
|
|
@@ -198,6 +208,18 @@ const styles = StyleSheet.create({
|
|
|
198
208
|
fontSize: 14,
|
|
199
209
|
fontWeight: '500',
|
|
200
210
|
},
|
|
211
|
+
settingsButton: {
|
|
212
|
+
width: 32,
|
|
213
|
+
height: 32,
|
|
214
|
+
borderRadius: 16,
|
|
215
|
+
backgroundColor: Colors.background,
|
|
216
|
+
alignItems: 'center',
|
|
217
|
+
justifyContent: 'center',
|
|
218
|
+
},
|
|
219
|
+
settingsButtonText: {
|
|
220
|
+
fontSize: 16,
|
|
221
|
+
color: Colors.textSecondary,
|
|
222
|
+
},
|
|
201
223
|
closeButton: {
|
|
202
224
|
width: 30,
|
|
203
225
|
height: 30,
|
|
@@ -4,8 +4,6 @@ import {
|
|
|
4
4
|
Text,
|
|
5
5
|
StyleSheet,
|
|
6
6
|
Animated,
|
|
7
|
-
PanResponder,
|
|
8
|
-
Easing,
|
|
9
7
|
} from 'react-native';
|
|
10
8
|
import type { AnyDebugFeature } from '../../types';
|
|
11
9
|
import { getPreference, setPreference, KEYS } from '../../utils/debugPreferences';
|
|
@@ -13,6 +11,7 @@ import { FloatIcon } from '../floating/FloatIcon';
|
|
|
13
11
|
import { DebugPanel } from './DebugPanel';
|
|
14
12
|
import { FeatureTabBar } from './FeatureTabBar';
|
|
15
13
|
import type { TabItem } from './FeatureTabBar';
|
|
14
|
+
import { useTabAnimation } from './useTabAnimation';
|
|
16
15
|
|
|
17
16
|
// ─── Error Boundary ────────────────────────────────────
|
|
18
17
|
interface ErrorBoundaryState {
|
|
@@ -66,34 +65,14 @@ export function FloatPanelView({ features, panelOpen, onOpenPanel, onClosePanel,
|
|
|
66
65
|
return () => { mounted = false; };
|
|
67
66
|
}, []);
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const featuresLengthRef = useRef(features.length);
|
|
78
|
-
featuresLengthRef.current = features.length;
|
|
79
|
-
const switchTabRef = useRef<(index: number) => void>(() => {});
|
|
80
|
-
|
|
81
|
-
// Swipe-to-switch responder
|
|
82
|
-
const swipeResponder = useRef(
|
|
83
|
-
PanResponder.create({
|
|
84
|
-
onStartShouldSetPanResponder: () => false,
|
|
85
|
-
onMoveShouldSetPanResponder: (_, gs) => {
|
|
86
|
-
if (isSwitchingTab.current) return false;
|
|
87
|
-
return Math.abs(gs.dx) > 25 && Math.abs(gs.dx) > Math.abs(gs.dy) * 2.5;
|
|
88
|
-
},
|
|
89
|
-
onPanResponderRelease: (_, gs) => {
|
|
90
|
-
const tab = activeTabRef.current;
|
|
91
|
-
if (gs.dx < -40 && tab < featuresLengthRef.current - 1) switchTabRef.current(tab + 1);
|
|
92
|
-
else if (gs.dx > 40 && tab > 0) switchTabRef.current(tab - 1);
|
|
93
|
-
},
|
|
94
|
-
onPanResponderTerminationRequest: () => true,
|
|
95
|
-
}),
|
|
96
|
-
).current;
|
|
68
|
+
const { contentOpacity, contentTranslateX, panHandlers, switchTab } = useTabAnimation({
|
|
69
|
+
activeTab,
|
|
70
|
+
tabCount: features.length,
|
|
71
|
+
onTabChange: useCallback((index: number) => {
|
|
72
|
+
setActiveTab(index);
|
|
73
|
+
setPreference(KEYS.lastTab, String(index));
|
|
74
|
+
}, []),
|
|
75
|
+
});
|
|
97
76
|
|
|
98
77
|
// Feature subscription → re-render on data changes
|
|
99
78
|
const [, setTick] = useState(0);
|
|
@@ -122,43 +101,6 @@ export function FloatPanelView({ features, panelOpen, onOpenPanel, onClosePanel,
|
|
|
122
101
|
}
|
|
123
102
|
}, [features.length, activeTab]);
|
|
124
103
|
|
|
125
|
-
// Tab switching with content animation
|
|
126
|
-
const switchTab = useCallback(
|
|
127
|
-
(index: number) => {
|
|
128
|
-
if (isSwitchingTab.current || index === activeTabRef.current) return;
|
|
129
|
-
isSwitchingTab.current = true;
|
|
130
|
-
const direction = index > activeTabRef.current ? 1 : -1;
|
|
131
|
-
|
|
132
|
-
Animated.parallel([
|
|
133
|
-
Animated.timing(contentOpacity, { toValue: 0, duration: 80, useNativeDriver: true }),
|
|
134
|
-
Animated.timing(contentTranslateX, {
|
|
135
|
-
toValue: -direction * 40,
|
|
136
|
-
duration: 80,
|
|
137
|
-
useNativeDriver: true,
|
|
138
|
-
}),
|
|
139
|
-
]).start(() => {
|
|
140
|
-
setActiveTab(index);
|
|
141
|
-
setPreference(KEYS.lastTab, String(index));
|
|
142
|
-
contentTranslateX.setValue(direction * 40);
|
|
143
|
-
Animated.parallel([
|
|
144
|
-
Animated.timing(contentOpacity, { toValue: 1, duration: 150, useNativeDriver: true }),
|
|
145
|
-
Animated.timing(contentTranslateX, {
|
|
146
|
-
toValue: 0,
|
|
147
|
-
duration: 200,
|
|
148
|
-
easing: Easing.out(Easing.cubic),
|
|
149
|
-
useNativeDriver: true,
|
|
150
|
-
}),
|
|
151
|
-
]).start(() => {
|
|
152
|
-
isSwitchingTab.current = false;
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
},
|
|
156
|
-
[contentOpacity, contentTranslateX],
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
// Keep ref in sync
|
|
160
|
-
switchTabRef.current = switchTab;
|
|
161
|
-
|
|
162
104
|
// Badge (first feature that returns one)
|
|
163
105
|
const envBadge = features.map((f) => f.badge?.()).find((b) => b != null) ?? null;
|
|
164
106
|
const tabs: TabItem[] = features.map((f) => ({ label: f.label, id: f.name }));
|
|
@@ -195,7 +137,7 @@ export function FloatPanelView({ features, panelOpen, onOpenPanel, onClosePanel,
|
|
|
195
137
|
styles.contentContainer,
|
|
196
138
|
{ opacity: contentOpacity, transform: [{ translateX: contentTranslateX }] },
|
|
197
139
|
]}
|
|
198
|
-
{...
|
|
140
|
+
{...panHandlers}
|
|
199
141
|
>
|
|
200
142
|
{renderFeatureContent()}
|
|
201
143
|
</Animated.View>
|