react-native-debug-toolkit 3.1.5 → 3.2.1
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 +12 -5
- package/README.zh-CN.md +12 -5
- package/lib/commonjs/core/initialize.js +5 -3
- package/lib/commonjs/core/initialize.js.map +1 -1
- package/lib/commonjs/features/devConnect/DevConnectQrScanner.js +203 -0
- package/lib/commonjs/features/devConnect/DevConnectQrScanner.js.map +1 -0
- package/lib/commonjs/features/devConnect/DevConnectTab.js +541 -0
- package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -0
- package/lib/commonjs/features/devConnect/cameraKit.js +54 -0
- package/lib/commonjs/features/devConnect/cameraKit.js.map +1 -0
- package/lib/commonjs/features/devConnect/devConnectPreferences.js +35 -0
- package/lib/commonjs/features/devConnect/devConnectPreferences.js.map +1 -0
- package/lib/commonjs/features/devConnect/devConnectUtils.js +53 -0
- package/lib/commonjs/features/devConnect/devConnectUtils.js.map +1 -0
- package/lib/commonjs/features/devConnect/index.js +92 -0
- package/lib/commonjs/features/devConnect/index.js.map +1 -0
- package/lib/commonjs/features/devConnect/platformDetect.js +30 -0
- package/lib/commonjs/features/devConnect/platformDetect.js.map +1 -0
- package/lib/commonjs/features/devConnect/types.js +2 -0
- package/lib/commonjs/features/devConnect/types.js.map +1 -0
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/DebugView.js +1 -0
- package/lib/commonjs/ui/DebugView.js.map +1 -1
- package/lib/commonjs/ui/panel/DebugPanel.js +0 -25
- package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
- package/lib/commonjs/utils/debugPreferences.js +2 -1
- package/lib/commonjs/utils/debugPreferences.js.map +1 -1
- package/lib/module/core/initialize.js +5 -3
- package/lib/module/core/initialize.js.map +1 -1
- package/lib/module/features/devConnect/DevConnectQrScanner.js +198 -0
- package/lib/module/features/devConnect/DevConnectQrScanner.js.map +1 -0
- package/lib/module/features/devConnect/DevConnectTab.js +536 -0
- package/lib/module/features/devConnect/DevConnectTab.js.map +1 -0
- package/lib/module/features/devConnect/cameraKit.js +49 -0
- package/lib/module/features/devConnect/cameraKit.js.map +1 -0
- package/lib/module/features/devConnect/devConnectPreferences.js +29 -0
- package/lib/module/features/devConnect/devConnectPreferences.js.map +1 -0
- package/lib/module/features/devConnect/devConnectUtils.js +47 -0
- package/lib/module/features/devConnect/devConnectUtils.js.map +1 -0
- package/lib/module/features/devConnect/index.js +52 -0
- package/lib/module/features/devConnect/index.js.map +1 -0
- package/lib/module/features/devConnect/platformDetect.js +26 -0
- package/lib/module/features/devConnect/platformDetect.js.map +1 -0
- package/lib/module/features/devConnect/types.js +2 -0
- package/lib/module/features/devConnect/types.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/DebugView.js +1 -0
- package/lib/module/ui/DebugView.js.map +1 -1
- package/lib/module/ui/panel/DebugPanel.js +1 -26
- package/lib/module/ui/panel/DebugPanel.js.map +1 -1
- package/lib/module/utils/debugPreferences.js +2 -1
- package/lib/module/utils/debugPreferences.js.map +1 -1
- package/lib/typescript/src/core/initialize.d.ts +1 -0
- package/lib/typescript/src/core/initialize.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts +9 -0
- package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts +5 -0
- package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/cameraKit.d.ts +47 -0
- package/lib/typescript/src/features/devConnect/cameraKit.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts +7 -0
- package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts +12 -0
- package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/index.d.ts +7 -0
- package/lib/typescript/src/features/devConnect/index.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/platformDetect.d.ts +2 -0
- package/lib/typescript/src/features/devConnect/platformDetect.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/types.d.ts +7 -0
- package/lib/typescript/src/features/devConnect/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/feature.d.ts +1 -1
- package/lib/typescript/src/types/feature.d.ts.map +1 -1
- package/lib/typescript/src/ui/DebugView.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
- package/lib/typescript/src/utils/debugPreferences.d.ts +1 -0
- package/lib/typescript/src/utils/debugPreferences.d.ts.map +1 -1
- package/node/daemon/src/console/console.html +63 -15
- package/package.json +10 -2
- package/src/core/initialize.ts +7 -1
- package/src/features/devConnect/DevConnectQrScanner.tsx +173 -0
- package/src/features/devConnect/DevConnectTab.tsx +437 -0
- package/src/features/devConnect/cameraKit.ts +93 -0
- package/src/features/devConnect/devConnectPreferences.ts +33 -0
- package/src/features/devConnect/devConnectUtils.ts +59 -0
- package/src/features/devConnect/index.ts +64 -0
- package/src/features/devConnect/platformDetect.ts +26 -0
- package/src/features/devConnect/types.ts +6 -0
- package/src/index.ts +2 -0
- package/src/types/feature.ts +2 -1
- package/src/ui/DebugView.tsx +1 -0
- package/src/ui/panel/DebugPanel.tsx +1 -23
- package/src/utils/debugPreferences.ts +1 -0
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js +0 -495
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +0 -1
- package/lib/module/ui/panel/StreamingSettingsModal.js +0 -490
- package/lib/module/ui/panel/StreamingSettingsModal.js.map +0 -1
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +0 -8
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +0 -1
- package/src/ui/panel/StreamingSettingsModal.tsx +0 -528
|
@@ -346,6 +346,7 @@ header h1 span{color:var(--text3);font-weight:400}
|
|
|
346
346
|
padding:0 8px;height:100%;display:flex;align-items:center;justify-content:center;
|
|
347
347
|
color:var(--text3);font-size:10px;transition:transform .2s,color .15s;
|
|
348
348
|
}
|
|
349
|
+
.log-expand.open{transform:rotate(90deg)}
|
|
349
350
|
.log-entry:hover .log-expand{color:var(--cyan)}
|
|
350
351
|
.log-entry.expanded .log-expand{transform:rotate(90deg);color:var(--cyan)}
|
|
351
352
|
|
|
@@ -363,15 +364,20 @@ header h1 span{color:var(--text3);font-weight:400}
|
|
|
363
364
|
.detail-sections{display:flex;flex-direction:column;gap:10px}
|
|
364
365
|
.detail-section{
|
|
365
366
|
border:1px solid var(--border);border-radius:var(--radius);
|
|
366
|
-
background:
|
|
367
|
+
background:var(--bg);overflow:hidden;
|
|
367
368
|
}
|
|
368
369
|
.detail-section-header{
|
|
369
370
|
display:flex;align-items:center;justify-content:space-between;
|
|
370
|
-
padding:
|
|
371
|
-
background:rgba(0,229,255,.
|
|
371
|
+
padding:9px 14px 9px 18px;border-bottom:1px solid var(--border);
|
|
372
|
+
background:linear-gradient(135deg,rgba(0,229,255,.07) 0%,rgba(0,229,255,.02) 100%);
|
|
373
|
+
position:relative;
|
|
374
|
+
}
|
|
375
|
+
.detail-section-header::before{
|
|
376
|
+
content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
|
|
377
|
+
background:var(--cyan);border-radius:0 2px 2px 0;
|
|
372
378
|
}
|
|
373
379
|
.detail-section-title{
|
|
374
|
-
font-size:
|
|
380
|
+
font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;
|
|
375
381
|
color:var(--cyan);font-family:var(--font-mono);
|
|
376
382
|
}
|
|
377
383
|
.detail-section-copy{
|
|
@@ -380,7 +386,7 @@ header h1 span{color:var(--text3);font-weight:400}
|
|
|
380
386
|
transition:color .15s;
|
|
381
387
|
}
|
|
382
388
|
.detail-section-copy:hover{color:var(--cyan)}
|
|
383
|
-
.detail-section-body{padding:
|
|
389
|
+
.detail-section-body{padding:10px 14px 10px 18px}
|
|
384
390
|
|
|
385
391
|
/* Detail key-value table */
|
|
386
392
|
.detail-table{width:100%;border-collapse:collapse}
|
|
@@ -429,25 +435,30 @@ header h1 span{color:var(--text3);font-weight:400}
|
|
|
429
435
|
.network-meta-line span{white-space:nowrap}
|
|
430
436
|
|
|
431
437
|
/* Collapsible sections */
|
|
432
|
-
.collapse-section{border:1px solid var(--border);border-radius:var(--radius);background:
|
|
438
|
+
.collapse-section{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg);overflow:hidden;margin-bottom:0}
|
|
433
439
|
.collapse-header{
|
|
434
440
|
display:flex;align-items:center;justify-content:space-between;
|
|
435
|
-
padding:
|
|
436
|
-
background:rgba(0,229,255,.
|
|
441
|
+
padding:9px 14px 9px 18px;border-bottom:1px solid var(--border);
|
|
442
|
+
background:linear-gradient(135deg,rgba(0,229,255,.07) 0%,rgba(0,229,255,.02) 100%);
|
|
443
|
+
cursor:pointer;user-select:none;position:relative;
|
|
444
|
+
}
|
|
445
|
+
.collapse-header::before{
|
|
446
|
+
content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
|
|
447
|
+
background:var(--cyan);border-radius:0 2px 2px 0;
|
|
437
448
|
}
|
|
438
|
-
.collapse-header:hover{background:rgba(0,229,255,.
|
|
439
|
-
.collapse-header-left{display:flex;align-items:center;gap:
|
|
449
|
+
.collapse-header:hover{background:linear-gradient(135deg,rgba(0,229,255,.12) 0%,rgba(0,229,255,.04) 100%)}
|
|
450
|
+
.collapse-header-left{display:flex;align-items:center;gap:8px}
|
|
440
451
|
.collapse-arrow{
|
|
441
|
-
font-size:
|
|
452
|
+
font-size:10px;color:var(--cyan);transition:transform .2s;display:inline-block;opacity:.7;
|
|
442
453
|
}
|
|
443
|
-
.collapse-arrow.open{transform:rotate(90deg)}
|
|
454
|
+
.collapse-arrow.open{transform:rotate(90deg);opacity:1}
|
|
444
455
|
.collapse-title{
|
|
445
|
-
font-size:
|
|
456
|
+
font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;
|
|
446
457
|
color:var(--cyan);font-family:var(--font-mono);
|
|
447
458
|
}
|
|
448
459
|
.collapse-body{display:none;padding:0}
|
|
449
460
|
.collapse-body.open{display:block}
|
|
450
|
-
.section-body-inner{padding:
|
|
461
|
+
.section-body-inner{padding:10px 14px 10px 18px}
|
|
451
462
|
.method-badge{
|
|
452
463
|
font-family:var(--font-mono);font-size:11px;font-weight:700;
|
|
453
464
|
padding:3px 10px;border-radius:3px;letter-spacing:.04em;
|
|
@@ -908,6 +919,35 @@ mark{
|
|
|
908
919
|
entry.data);
|
|
909
920
|
}
|
|
910
921
|
|
|
922
|
+
function estimateByteSize(value) {
|
|
923
|
+
if (value == null) return 0;
|
|
924
|
+
if (typeof value === 'string') return new Blob([value]).size;
|
|
925
|
+
try { return new Blob([JSON.stringify(value)]).size; } catch { return 0; }
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function formatSize(bytes) {
|
|
929
|
+
if (!bytes || bytes <= 0) return '';
|
|
930
|
+
if (bytes < 1024) return bytes + ' B';
|
|
931
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
932
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function getNetworkSize(entry) {
|
|
936
|
+
var request = readObject(entry.request) || entry;
|
|
937
|
+
var response = readObject(entry.response);
|
|
938
|
+
var respSize = 0;
|
|
939
|
+
if (response) {
|
|
940
|
+
var headers = response.headers;
|
|
941
|
+
if (headers) {
|
|
942
|
+
var cl = headers['content-length'] || headers['Content-Length'] || headers['contentLength'] || headers['ContentLength'];
|
|
943
|
+
if (cl) { var n = parseInt(cl, 10); if (n > 0) respSize = n; }
|
|
944
|
+
}
|
|
945
|
+
if (!respSize && response.data != null) respSize = estimateByteSize(response.data);
|
|
946
|
+
}
|
|
947
|
+
var reqSize = request.body != null ? estimateByteSize(request.body) : 0;
|
|
948
|
+
return { req: reqSize, res: respSize };
|
|
949
|
+
}
|
|
950
|
+
|
|
911
951
|
function renderNetworkDetails(entry) {
|
|
912
952
|
var request = readObject(entry.request) || entry;
|
|
913
953
|
var response = readObject(entry.response);
|
|
@@ -924,6 +964,11 @@ mark{
|
|
|
924
964
|
metaParts.push(response.status + (response.statusText ? ' ' + response.statusText : ''));
|
|
925
965
|
}
|
|
926
966
|
if (entry.duration !== undefined) metaParts.push(entry.duration + 'ms');
|
|
967
|
+
var sizes = getNetworkSize(entry);
|
|
968
|
+
var sizeParts = [];
|
|
969
|
+
if (sizes.req > 0) sizeParts.push('req: ' + formatSize(sizes.req));
|
|
970
|
+
if (sizes.res > 0) sizeParts.push('res: ' + formatSize(sizes.res));
|
|
971
|
+
if (sizeParts.length) metaParts.push(sizeParts.join(' / '));
|
|
927
972
|
if (entry.timestamp) metaParts.push(formatTimeShort(new Date(entry.timestamp).toISOString()));
|
|
928
973
|
if (metaParts.length) {
|
|
929
974
|
html += '<div class="network-meta-line">' + metaParts.map(function(p) { return '<span>' + escapeHtml(p) + '</span>'; }).join('') + '</div>';
|
|
@@ -1308,7 +1353,7 @@ mark{
|
|
|
1308
1353
|
html += '</div>';
|
|
1309
1354
|
html += '<div class="log-status">' + statusBadge(entry) + '</div>';
|
|
1310
1355
|
html += '<div class="log-copy" onclick="event.stopPropagation();copyEntryJSON(\'' + rowId + '\')"><button class="copy-btn" title="Copy entry JSON">⎘</button></div>';
|
|
1311
|
-
html += '<div class="log-expand
|
|
1356
|
+
html += '<div class="log-expand' + (isExpanded ? ' open' : '') + '" id="arrow-' + rowId + '">▶</div>';
|
|
1312
1357
|
html += '</div>';
|
|
1313
1358
|
html += '<div class="log-detail" id="detail-' + rowId + '">';
|
|
1314
1359
|
html += '<div class="log-detail-inner"><div class="detail-sections">';
|
|
@@ -1479,12 +1524,15 @@ mark{
|
|
|
1479
1524
|
window.toggleRow = function(rowId) {
|
|
1480
1525
|
var entry = document.getElementById('entry-' + rowId);
|
|
1481
1526
|
var detail = document.getElementById('detail-' + rowId);
|
|
1527
|
+
var arrow = document.getElementById('arrow-' + rowId);
|
|
1482
1528
|
if (!entry || !detail) return;
|
|
1483
1529
|
expandedRows[rowId] = !expandedRows[rowId];
|
|
1484
1530
|
if (expandedRows[rowId]) {
|
|
1485
1531
|
entry.classList.add('expanded');
|
|
1532
|
+
if (arrow) arrow.classList.add('open');
|
|
1486
1533
|
} else {
|
|
1487
1534
|
entry.classList.remove('expanded');
|
|
1535
|
+
if (arrow) arrow.classList.remove('open');
|
|
1488
1536
|
}
|
|
1489
1537
|
};
|
|
1490
1538
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-debug-toolkit",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "A local-first React Native debug toolkit with Web Console, HTTP API, and MCP support for AI-readable app logs",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -52,11 +52,19 @@
|
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"@react-native-clipboard/clipboard": ">=1.0.0",
|
|
54
54
|
"react": ">=18.0.0",
|
|
55
|
-
"react-native": ">=0.72.0"
|
|
55
|
+
"react-native": ">=0.72.0",
|
|
56
|
+
"react-native-camera-kit": ">=18.0.0",
|
|
57
|
+
"expo-camera": ">=15.0.0"
|
|
56
58
|
},
|
|
57
59
|
"peerDependenciesMeta": {
|
|
58
60
|
"@react-native-clipboard/clipboard": {
|
|
59
61
|
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"react-native-camera-kit": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"expo-camera": {
|
|
67
|
+
"optional": true
|
|
60
68
|
}
|
|
61
69
|
},
|
|
62
70
|
"devDependencies": {
|
package/src/core/initialize.ts
CHANGED
|
@@ -11,6 +11,7 @@ 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 { createDevConnectFeature, restoreDevConnectSettingsToDaemon } from '../features/devConnect';
|
|
14
15
|
import { daemonClient } from '../utils/DaemonClient';
|
|
15
16
|
import type { AnyDebugFeature, BuiltInFeatureName } from '../types';
|
|
16
17
|
|
|
@@ -25,6 +26,7 @@ export interface FeatureConfigs {
|
|
|
25
26
|
track?: boolean | TrackFeatureConfig;
|
|
26
27
|
environment?: Parameters<typeof createEnvironmentFeature>[0];
|
|
27
28
|
clipboard?: boolean;
|
|
29
|
+
devConnect?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface InitializeOptions {
|
|
@@ -43,6 +45,7 @@ const featureRegistry: Record<BuiltInFeatureName, (config?: any) => AnyDebugFeat
|
|
|
43
45
|
track: createTrackFeature,
|
|
44
46
|
environment: createEnvironmentFeature,
|
|
45
47
|
clipboard: createClipboardFeature,
|
|
48
|
+
devConnect: createDevConnectFeature,
|
|
46
49
|
};
|
|
47
50
|
|
|
48
51
|
const DEFAULT_FEATURES: BuiltInFeatureName[] = [
|
|
@@ -52,6 +55,7 @@ const DEFAULT_FEATURES: BuiltInFeatureName[] = [
|
|
|
52
55
|
'zustand',
|
|
53
56
|
'track',
|
|
54
57
|
'clipboard',
|
|
58
|
+
'devConnect',
|
|
55
59
|
];
|
|
56
60
|
|
|
57
61
|
function resolveFeatureConfigs(configs: FeatureConfigs): AnyDebugFeature[] {
|
|
@@ -119,7 +123,9 @@ export function initializeDebugToolkit(
|
|
|
119
123
|
DebugToolkit.hideLauncher();
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
|
|
126
|
+
restoreDevConnectSettingsToDaemon()
|
|
127
|
+
.then(() => daemonClient.restore(), () => daemonClient.restore())
|
|
128
|
+
.catch(() => {});
|
|
123
129
|
|
|
124
130
|
return DebugToolkit;
|
|
125
131
|
} catch (error) {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React, { Component, useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
View,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
import { Colors } from '../../ui/theme/colors';
|
|
12
|
+
import {
|
|
13
|
+
getScannerModule,
|
|
14
|
+
type CameraKitReadCodeEvent,
|
|
15
|
+
type ExpoCameraScanResult,
|
|
16
|
+
} from './cameraKit';
|
|
17
|
+
import { parseMetroQrPayload } from './devConnectUtils';
|
|
18
|
+
|
|
19
|
+
// ─── Camera Error Boundary ─────────────────────────────────
|
|
20
|
+
|
|
21
|
+
interface CameraBoundaryProps {
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
onCameraError: (msg: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CameraBoundaryState {
|
|
27
|
+
hasError: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class CameraErrorBoundary extends Component<CameraBoundaryProps, CameraBoundaryState> {
|
|
31
|
+
state: CameraBoundaryState = { hasError: false };
|
|
32
|
+
|
|
33
|
+
static getDerivedStateFromError(): CameraBoundaryState {
|
|
34
|
+
return { hasError: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
componentDidCatch(error: Error) {
|
|
38
|
+
console.warn('[DevConnect] Camera error:', error.message);
|
|
39
|
+
this.props.onCameraError(error.message || 'Camera failed to initialize.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render() {
|
|
43
|
+
if (this.state.hasError) return null;
|
|
44
|
+
return this.props.children;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── QR Scanner ─────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
interface DevConnectQrScannerProps {
|
|
51
|
+
visible: boolean;
|
|
52
|
+
onClose: () => void;
|
|
53
|
+
onScanHost: (host: string) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function DevConnectQrScanner({ visible, onClose, onScanHost }: DevConnectQrScannerProps) {
|
|
57
|
+
const scannedRef = useRef(false);
|
|
58
|
+
const [error, setError] = useState<string | null>(null);
|
|
59
|
+
const [cameraFailed, setCameraFailed] = useState(false);
|
|
60
|
+
const scanner = getScannerModule();
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (visible) {
|
|
64
|
+
scannedRef.current = false;
|
|
65
|
+
setError(null);
|
|
66
|
+
setCameraFailed(false);
|
|
67
|
+
}
|
|
68
|
+
}, [visible]);
|
|
69
|
+
|
|
70
|
+
const handleScanned = useCallback((rawValue: string) => {
|
|
71
|
+
if (scannedRef.current) return;
|
|
72
|
+
if (typeof rawValue !== 'string') return;
|
|
73
|
+
|
|
74
|
+
const parsed = parseMetroQrPayload(rawValue);
|
|
75
|
+
if (!parsed) {
|
|
76
|
+
setError('QR code does not contain a supported Metro URL.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
scannedRef.current = true;
|
|
81
|
+
setError(null);
|
|
82
|
+
onScanHost(parsed.computerHost);
|
|
83
|
+
onClose();
|
|
84
|
+
}, [onClose, onScanHost]);
|
|
85
|
+
|
|
86
|
+
const handleCameraKitRead = useCallback((event: CameraKitReadCodeEvent) => {
|
|
87
|
+
handleScanned(event.nativeEvent?.codeStringValue ?? '');
|
|
88
|
+
}, [handleScanned]);
|
|
89
|
+
|
|
90
|
+
const handleExpoScanned = useCallback((result: ExpoCameraScanResult) => {
|
|
91
|
+
handleScanned(result.value ?? '');
|
|
92
|
+
}, [handleScanned]);
|
|
93
|
+
|
|
94
|
+
const handleCameraError = useCallback((_msg: string) => {
|
|
95
|
+
setCameraFailed(true);
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
if (!visible || !scanner) return null;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Modal visible={visible} animationType="slide" onRequestClose={onClose}>
|
|
102
|
+
<View style={styles.container}>
|
|
103
|
+
{!cameraFailed && (
|
|
104
|
+
<CameraErrorBoundary onCameraError={handleCameraError}>
|
|
105
|
+
{scanner.kind === 'camera-kit' && scanner.CameraKit ? (
|
|
106
|
+
<scanner.CameraKit.Camera
|
|
107
|
+
style={styles.camera}
|
|
108
|
+
cameraType={scanner.CameraKit.CameraType?.Back}
|
|
109
|
+
scanBarcode
|
|
110
|
+
onReadCode={handleCameraKitRead}
|
|
111
|
+
showFrame
|
|
112
|
+
laserColor={Colors.primary}
|
|
113
|
+
frameColor={Colors.primary}
|
|
114
|
+
allowedBarcodeTypes={['qr']}
|
|
115
|
+
/>
|
|
116
|
+
) : scanner.kind === 'expo-camera' && scanner.ExpoCamera ? (
|
|
117
|
+
<scanner.ExpoCamera.Camera
|
|
118
|
+
style={styles.camera}
|
|
119
|
+
onBarCodeScanned={handleExpoScanned}
|
|
120
|
+
barCodeScannerSettings={{ barCodeTypes: ['qr'] }}
|
|
121
|
+
/>
|
|
122
|
+
) : null}
|
|
123
|
+
</CameraErrorBoundary>
|
|
124
|
+
)}
|
|
125
|
+
{cameraFailed && (
|
|
126
|
+
<View style={styles.cameraFallback}>
|
|
127
|
+
<Text style={styles.cameraFallbackText}>Camera unavailable.</Text>
|
|
128
|
+
<Text style={styles.cameraFallbackHint}>Please enter computer IP manually.</Text>
|
|
129
|
+
</View>
|
|
130
|
+
)}
|
|
131
|
+
<View style={styles.footer}>
|
|
132
|
+
{!cameraFailed && !error && <Text style={styles.hint}>Scan a Metro QR code.</Text>}
|
|
133
|
+
{error && <Text style={styles.error}>{error}</Text>}
|
|
134
|
+
<TouchableOpacity style={styles.closeButton} onPress={onClose} activeOpacity={0.7}>
|
|
135
|
+
<Text style={styles.closeButtonText}>Close</Text>
|
|
136
|
+
</TouchableOpacity>
|
|
137
|
+
</View>
|
|
138
|
+
<Pressable style={styles.topClose} onPress={onClose}>
|
|
139
|
+
<Text style={styles.topCloseText}>Close</Text>
|
|
140
|
+
</Pressable>
|
|
141
|
+
</View>
|
|
142
|
+
</Modal>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const styles = StyleSheet.create({
|
|
147
|
+
container: { flex: 1, backgroundColor: '#000' },
|
|
148
|
+
camera: { flex: 1 },
|
|
149
|
+
cameraFallback: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
|
|
150
|
+
cameraFallbackText: { fontSize: 16, color: '#fff', fontWeight: '600', marginBottom: 8 },
|
|
151
|
+
cameraFallbackHint: { fontSize: 13, color: 'rgba(255,255,255,0.6)', textAlign: 'center' },
|
|
152
|
+
footer: { padding: 16, backgroundColor: Colors.surface },
|
|
153
|
+
hint: { fontSize: 13, color: Colors.textSecondary, marginBottom: 12 },
|
|
154
|
+
error: { fontSize: 13, color: Colors.error, marginBottom: 12 },
|
|
155
|
+
closeButton: {
|
|
156
|
+
alignItems: 'center',
|
|
157
|
+
justifyContent: 'center',
|
|
158
|
+
paddingVertical: 11,
|
|
159
|
+
borderRadius: 10,
|
|
160
|
+
backgroundColor: Colors.primary,
|
|
161
|
+
},
|
|
162
|
+
closeButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' },
|
|
163
|
+
topClose: {
|
|
164
|
+
position: 'absolute',
|
|
165
|
+
top: 48,
|
|
166
|
+
right: 16,
|
|
167
|
+
paddingHorizontal: 12,
|
|
168
|
+
paddingVertical: 8,
|
|
169
|
+
borderRadius: 8,
|
|
170
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
171
|
+
},
|
|
172
|
+
topCloseText: { color: '#fff', fontSize: 13, fontWeight: '600' },
|
|
173
|
+
});
|