@pickleball/expo-sdk 0.2.0 → 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/CHANGELOG.md +41 -0
- package/README.md +155 -101
- package/dist/advanced-DcbHBCGb.d.ts +300 -0
- package/dist/advanced-DeQwg7MK.d.cts +300 -0
- package/dist/advanced.cjs +14 -2
- package/dist/advanced.d.cts +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/advanced.js +17 -5
- package/dist/camera-controls-By8tHigY.d.cts +97 -0
- package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
- package/dist/chunk-62YV3PCD.cjs +139 -0
- package/dist/chunk-6F3HN6US.js +50 -0
- package/dist/chunk-6YE5BL3A.cjs +50 -0
- package/dist/chunk-7Z7TN5N3.js +318 -0
- package/dist/{chunk-TRLASNPQ.cjs → chunk-GXQYMPHE.cjs} +1 -1
- package/dist/{chunk-TBPR2QND.cjs → chunk-IUIRVKJI.cjs} +736 -399
- package/dist/chunk-TBUEXRFQ.cjs +318 -0
- package/dist/{chunk-FODE2W2H.js → chunk-XGP2MN5R.js} +688 -351
- package/dist/{chunk-6JOMESNT.js → chunk-XL4CISVQ.js} +1 -1
- package/dist/chunk-ZSKSXOCZ.js +139 -0
- package/dist/convex-command-subscription-Cabh9tND.d.ts +41 -0
- package/dist/convex-command-subscription-DUgpu75l.d.cts +41 -0
- package/dist/device-agent/index.cjs +973 -44
- package/dist/device-agent/index.d.cts +339 -3
- package/dist/device-agent/index.d.ts +339 -3
- package/dist/device-agent/index.js +940 -11
- package/dist/{engine-CCSSqPbZ.d.cts → engine-BSvSdCSE.d.cts} +227 -2
- package/dist/{engine-CCSSqPbZ.d.ts → engine-BSvSdCSE.d.ts} +227 -2
- package/dist/index.cjs +419 -937
- package/dist/index.d.cts +315 -32
- package/dist/index.d.ts +315 -32
- package/dist/index.js +423 -941
- package/dist/plugin.cjs +11 -13
- package/dist/plugin.d.cts +15 -1
- package/dist/plugin.d.ts +15 -1
- package/dist/plugin.js +7 -9
- package/dist/realtime/index.cjs +31 -0
- package/dist/realtime/index.d.cts +15 -0
- package/dist/realtime/index.d.ts +15 -0
- package/dist/realtime/index.js +31 -0
- package/dist/ui/index.cjs +1297 -0
- package/dist/ui/index.d.cts +493 -0
- package/dist/ui/index.d.ts +493 -0
- package/dist/ui/index.js +1297 -0
- package/dist/version.cjs +2 -2
- package/dist/version.js +1 -1
- package/package.json +31 -15
- package/src/advanced.ts +11 -9
- package/src/auto-resume.tsx +259 -0
- package/src/camera-controls.ts +99 -0
- package/src/camera-grant-provider.ts +392 -0
- package/src/camera-settings.ts +133 -0
- package/src/contracts.ts +73 -0
- package/src/device-agent/agent.ts +262 -9
- package/src/device-agent/beat-policy.ts +46 -0
- package/src/device-agent/component.tsx +11 -0
- package/src/device-agent/convex-beat-transport.ts +140 -0
- package/src/device-agent/convex-command-subscription.ts +101 -0
- package/src/device-agent/convex-control-channel.ts +7 -1
- package/src/device-agent/http-transport.ts +22 -1
- package/src/device-agent/index.ts +22 -0
- package/src/device-agent/mqtt-channel.ts +379 -0
- package/src/device-agent/mqtt-codec.ts +347 -0
- package/src/device-agent/tournament.ts +352 -0
- package/src/engine.ts +580 -15
- package/src/http-session-provider.ts +21 -189
- package/src/index.ts +84 -5
- package/src/local-video-quality.ts +43 -0
- package/src/native-runtime.ts +234 -78
- package/src/plugin.ts +19 -6
- package/src/provider.tsx +94 -18
- package/src/realtime/index.ts +56 -0
- package/src/resume-policy.ts +85 -0
- package/src/resume.ts +132 -0
- package/src/rtmp-room-adapter.ts +164 -12
- package/src/session-grant.ts +256 -0
- package/src/ui/camera-screen.tsx +484 -0
- package/src/ui/controls-sheet.tsx +445 -0
- package/src/ui/copy.ts +326 -0
- package/src/ui/helpers.ts +105 -0
- package/src/ui/hooks.ts +59 -0
- package/src/ui/index.ts +42 -0
- package/src/ui/preview-view.tsx +39 -0
- package/src/ui/primitives.tsx +304 -0
- package/src/uplink-check.ts +173 -0
- package/dist/advanced-BXDrDl1h.d.ts +0 -316
- package/dist/advanced-BdAa2Erv.d.cts +0 -316
- package/src/livekit-room-adapter.ts +0 -390
- package/src/livestream-screen.tsx +0 -1002
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import React, { useEffect, useState, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
Platform,
|
|
5
|
+
Pressable,
|
|
6
|
+
ScrollView,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Switch,
|
|
9
|
+
Text,
|
|
10
|
+
View,
|
|
11
|
+
useWindowDimensions,
|
|
12
|
+
} from "react-native";
|
|
13
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
14
|
+
import {
|
|
15
|
+
setExposureBias,
|
|
16
|
+
setExposureLocked,
|
|
17
|
+
setFocusLocked,
|
|
18
|
+
setTorch,
|
|
19
|
+
setZoom,
|
|
20
|
+
} from "../camera-controls";
|
|
21
|
+
import { useCameraSettings, type CameraSettings, type CameraSettingsStore } from "../camera-settings";
|
|
22
|
+
import { useLivestream } from "../provider";
|
|
23
|
+
import { runUplinkCheck, type UplinkCheckResult } from "../uplink-check";
|
|
24
|
+
import { uiCopy, type UiLocale } from "./copy";
|
|
25
|
+
import { useCameraCapabilities } from "./hooks";
|
|
26
|
+
import {
|
|
27
|
+
CameraOption,
|
|
28
|
+
ControlRow,
|
|
29
|
+
Note,
|
|
30
|
+
PresetButton,
|
|
31
|
+
Segmented,
|
|
32
|
+
uiColors,
|
|
33
|
+
uiStyles,
|
|
34
|
+
} from "./primitives";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sheet cấu hình camera — menu ••• của Picklive Camera chuyển vào SDK, trừ ba
|
|
38
|
+
* mục kiosk (Nguồn, Thoát kiosk, badge OTA) mà app fleet đưa lại qua `footer`.
|
|
39
|
+
*
|
|
40
|
+
* Zoom/EV/khoá/đèn pin gọi thẳng native (camera-controls, typeof-guard) — tinh
|
|
41
|
+
* chỉnh camera runtime, không phải lifecycle phiên. Resolution/FPS/camera mặc
|
|
42
|
+
* định ghi vào CameraSettingsStore; app remount PickleballLiveProvider theo
|
|
43
|
+
* `cameraSettingsKey` để engine re-prepare.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const ZOOM_PRESETS = [1, 2, 3, 5] as const;
|
|
47
|
+
const EV_PRESETS = [-2, -1, 0, 1, 2] as const;
|
|
48
|
+
/** Tập tối đa; mức nào hiện thật do capabilities của lens quyết định. */
|
|
49
|
+
const FPS_CHOICES: Array<{ value: CameraSettings["fps"]; label: string }> = [
|
|
50
|
+
{ value: 24, label: "24" },
|
|
51
|
+
{ value: 30, label: "30" },
|
|
52
|
+
{ value: 60, label: "60" },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export interface CameraControlsSheetSections {
|
|
56
|
+
quality?: boolean;
|
|
57
|
+
antiFlicker?: boolean;
|
|
58
|
+
camera?: boolean;
|
|
59
|
+
zoom?: boolean;
|
|
60
|
+
exposure?: boolean;
|
|
61
|
+
locks?: boolean;
|
|
62
|
+
torch?: boolean;
|
|
63
|
+
uplink?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CameraControlsSheetProps {
|
|
67
|
+
visible: boolean;
|
|
68
|
+
onClose: () => void;
|
|
69
|
+
settings: CameraSettingsStore;
|
|
70
|
+
/** Ẩn mục không cần (mặc định hiện tất cả). */
|
|
71
|
+
sections?: CameraControlsSheetSections;
|
|
72
|
+
/** Đo mạng: đích POST + HEAD đo RTT (VD ingest của bạn). */
|
|
73
|
+
uplink?: { uploadUrl?: string; pingUrl?: string };
|
|
74
|
+
/**
|
|
75
|
+
* "side": panel nửa phải (màn ngang); "bottom": bottom-sheet (màn dọc);
|
|
76
|
+
* "auto" (mặc định) theo kích thước cửa sổ.
|
|
77
|
+
*/
|
|
78
|
+
layout?: "auto" | "side" | "bottom";
|
|
79
|
+
title?: string;
|
|
80
|
+
/** Hàng thêm của app ở cuối sheet (Picklive Camera: Nguồn, Thoát kiosk, badge OTA). */
|
|
81
|
+
footer?: ReactNode;
|
|
82
|
+
locale?: UiLocale;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const IN_SESSION = new Set(["connecting", "standby", "live", "reconnecting", "ending"]);
|
|
86
|
+
const UPLINK_BUSY = new Set(["connecting", "live", "reconnecting", "ending"]);
|
|
87
|
+
|
|
88
|
+
export function CameraControlsSheet(props: CameraControlsSheetProps) {
|
|
89
|
+
const { visible, onClose, settings, uplink, footer } = props;
|
|
90
|
+
const sections = props.sections ?? {};
|
|
91
|
+
const show = (key: keyof CameraControlsSheetSections) => sections[key] !== false;
|
|
92
|
+
const live = useLivestream();
|
|
93
|
+
const locale = props.locale ?? live.locale;
|
|
94
|
+
const copy = uiCopy[locale].sheet;
|
|
95
|
+
const config = useCameraSettings(settings);
|
|
96
|
+
const { width, height } = useWindowDimensions();
|
|
97
|
+
const side =
|
|
98
|
+
props.layout === "side" || (props.layout !== "bottom" && width > height);
|
|
99
|
+
|
|
100
|
+
const caps = useCameraCapabilities(visible, live.selectedCameraId);
|
|
101
|
+
const [zoom, setZoomValue] = useState(1);
|
|
102
|
+
const [ev, setEvValue] = useState(0);
|
|
103
|
+
const [focusLocked, setFocusLockedValue] = useState(false);
|
|
104
|
+
const [exposureLocked, setExposureLockedValue] = useState(false);
|
|
105
|
+
const [torch, setTorchValue] = useState(false);
|
|
106
|
+
const [net, setNet] = useState<{
|
|
107
|
+
running: boolean;
|
|
108
|
+
result: UplinkCheckResult | null;
|
|
109
|
+
error: string | null;
|
|
110
|
+
}>({ running: false, result: null, error: null });
|
|
111
|
+
|
|
112
|
+
// Đổi resolution/fps/camera → remount provider + re-prepare — khoá khi đang trong phiên.
|
|
113
|
+
const configLocked = IN_SESSION.has(live.state);
|
|
114
|
+
// Đo mạng lúc ĐANG PHÁT là giành uplink với chính luồng video → cấm.
|
|
115
|
+
// Standby thì đo được: RTMP nối nhưng chưa publish, lưu lượng không đáng kể.
|
|
116
|
+
const netBusy = UPLINK_BUSY.has(live.state);
|
|
117
|
+
|
|
118
|
+
// Chỉ chào fps mà lens đang gắn chạy được. Native im lặng bỏ qua mức không
|
|
119
|
+
// hỗ trợ, nên nút chào bừa sẽ là nút chết: người dùng bấm 60, UI hiện 60,
|
|
120
|
+
// camera vẫn 30. Chưa đọc được capabilities thì giữ 24/30 như trước.
|
|
121
|
+
const supportedFps = caps?.supportedFrameRates ?? [24, 30];
|
|
122
|
+
const fpsOptions = FPS_CHOICES.filter((choice) => supportedFps.includes(choice.value));
|
|
123
|
+
|
|
124
|
+
// Đổi sang lens không có 60fps trong khi config đang để 60 → hạ về 30, nếu
|
|
125
|
+
// không Segmented không có ô nào sáng và người dùng kẹt ở giá trị vô hiệu.
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (configLocked || !caps) return;
|
|
128
|
+
if (!supportedFps.includes(config.fps)) settings.set({ fps: 30 });
|
|
129
|
+
}, [caps, config.fps, configLocked, settings, supportedFps]);
|
|
130
|
+
|
|
131
|
+
async function runNetworkCheck() {
|
|
132
|
+
setNet({ running: true, result: null, error: null });
|
|
133
|
+
try {
|
|
134
|
+
const result = await runUplinkCheck({
|
|
135
|
+
resolution: config.resolution,
|
|
136
|
+
...(uplink?.uploadUrl ? { uploadUrl: uplink.uploadUrl } : {}),
|
|
137
|
+
...(uplink?.pingUrl ? { pingUrl: uplink.pingUrl } : {}),
|
|
138
|
+
});
|
|
139
|
+
setNet({ running: false, result, error: null });
|
|
140
|
+
} catch {
|
|
141
|
+
setNet({ running: false, result: null, error: copy.checkError });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
// supportedOrientations đủ cả dọc lẫn ngang: mặc định của Modal là
|
|
147
|
+
// ["portrait"], app khoá landscape (Info.plist chỉ có LandscapeLeft/Right)
|
|
148
|
+
// → iOS không tìm được hướng chung và ABORT ngay khi mount modal
|
|
149
|
+
// ("Supported orientations has no common orientation with the application").
|
|
150
|
+
<Modal
|
|
151
|
+
visible={visible}
|
|
152
|
+
transparent
|
|
153
|
+
animationType="fade"
|
|
154
|
+
onRequestClose={onClose}
|
|
155
|
+
// Android: cho modal phủ cả vùng status/navigation bar rồi tự né bằng
|
|
156
|
+
// SafeAreaView — không né là thanh điều hướng đè lên hàng nút.
|
|
157
|
+
statusBarTranslucent
|
|
158
|
+
navigationBarTranslucent
|
|
159
|
+
supportedOrientations={[
|
|
160
|
+
"portrait",
|
|
161
|
+
"portrait-upside-down",
|
|
162
|
+
"landscape",
|
|
163
|
+
"landscape-left",
|
|
164
|
+
"landscape-right",
|
|
165
|
+
]}
|
|
166
|
+
>
|
|
167
|
+
<View style={side ? styles.overlaySide : styles.overlayBottom}>
|
|
168
|
+
<Pressable
|
|
169
|
+
style={styles.backdrop}
|
|
170
|
+
onPress={onClose}
|
|
171
|
+
accessibilityRole="button"
|
|
172
|
+
accessibilityLabel={copy.closeBackdrop}
|
|
173
|
+
/>
|
|
174
|
+
<SafeAreaView
|
|
175
|
+
edges={side ? ["top", "right", "bottom"] : ["bottom", "left", "right"]}
|
|
176
|
+
style={side ? styles.panelSide : styles.panelBottom}
|
|
177
|
+
>
|
|
178
|
+
<View style={styles.panelHeader}>
|
|
179
|
+
<Text style={styles.heading}>{props.title ?? copy.title}</Text>
|
|
180
|
+
<Pressable
|
|
181
|
+
onPress={onClose}
|
|
182
|
+
style={styles.closeX}
|
|
183
|
+
accessibilityRole="button"
|
|
184
|
+
accessibilityLabel={copy.close}
|
|
185
|
+
>
|
|
186
|
+
<Text style={styles.closeXText}>✕</Text>
|
|
187
|
+
</Pressable>
|
|
188
|
+
</View>
|
|
189
|
+
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
|
190
|
+
{show("quality") ? (
|
|
191
|
+
<>
|
|
192
|
+
<ControlRow label={copy.resolution}>
|
|
193
|
+
<Segmented
|
|
194
|
+
options={[
|
|
195
|
+
{ value: 720, label: "720p" },
|
|
196
|
+
{ value: 1080, label: "1080p" },
|
|
197
|
+
]}
|
|
198
|
+
value={config.resolution}
|
|
199
|
+
disabled={configLocked}
|
|
200
|
+
onChange={(value) => settings.set({ resolution: value })}
|
|
201
|
+
/>
|
|
202
|
+
</ControlRow>
|
|
203
|
+
<ControlRow label={copy.fps}>
|
|
204
|
+
<Segmented
|
|
205
|
+
options={fpsOptions}
|
|
206
|
+
value={config.fps}
|
|
207
|
+
disabled={configLocked}
|
|
208
|
+
onChange={(value) => settings.set({ fps: value })}
|
|
209
|
+
/>
|
|
210
|
+
</ControlRow>
|
|
211
|
+
{config.fps === 60 ? <Note>{copy.fps60Note}</Note> : null}
|
|
212
|
+
</>
|
|
213
|
+
) : null}
|
|
214
|
+
|
|
215
|
+
{/* Chống chớp đèn — Android-only (iOS chưa có API antibanding).
|
|
216
|
+
Không khoá theo phiên: là key trên repeating request, đổi giữa
|
|
217
|
+
sóng an toàn — thấy vằn là chỉnh ngay được. */}
|
|
218
|
+
{show("antiFlicker") && Platform.OS === "android" ? (
|
|
219
|
+
<>
|
|
220
|
+
<ControlRow label={copy.antiFlicker}>
|
|
221
|
+
<Segmented
|
|
222
|
+
options={[
|
|
223
|
+
{ value: "50hz", label: "50Hz" },
|
|
224
|
+
{ value: "60hz", label: "60Hz" },
|
|
225
|
+
{ value: "auto", label: copy.auto },
|
|
226
|
+
]}
|
|
227
|
+
value={config.antiFlicker}
|
|
228
|
+
onChange={(value) => settings.set({ antiFlicker: value })}
|
|
229
|
+
/>
|
|
230
|
+
</ControlRow>
|
|
231
|
+
<Note>{copy.antiFlickerNote}</Note>
|
|
232
|
+
</>
|
|
233
|
+
) : null}
|
|
234
|
+
{configLocked && (show("quality") || show("camera")) ? (
|
|
235
|
+
<Note>{copy.lockedNote}</Note>
|
|
236
|
+
) : null}
|
|
237
|
+
|
|
238
|
+
{/* Camera mặc định mỗi lần mở app — danh sách dọc kèm id vì máy
|
|
239
|
+
thật phơi ra nhiều camera TRÙNG NHÃN. */}
|
|
240
|
+
{show("camera") ? (
|
|
241
|
+
<>
|
|
242
|
+
<ControlRow label={copy.defaultCamera} />
|
|
243
|
+
<View style={[styles.cameraList, configLocked && uiStyles.disabled]}>
|
|
244
|
+
<CameraOption
|
|
245
|
+
label={copy.autoCamera}
|
|
246
|
+
hint={copy.autoCameraHint}
|
|
247
|
+
active={config.cameraId === null}
|
|
248
|
+
disabled={configLocked}
|
|
249
|
+
onPress={() => settings.set({ cameraId: null })}
|
|
250
|
+
/>
|
|
251
|
+
{live.cameras.map((camera) => (
|
|
252
|
+
<CameraOption
|
|
253
|
+
key={camera.deviceId}
|
|
254
|
+
label={camera.facing === "front" ? copy.frontCamera : copy.backCamera}
|
|
255
|
+
hint={copy.cameraIdHint(camera.deviceId)}
|
|
256
|
+
active={config.cameraId === camera.deviceId}
|
|
257
|
+
disabled={configLocked}
|
|
258
|
+
onPress={() => settings.set({ cameraId: camera.deviceId })}
|
|
259
|
+
/>
|
|
260
|
+
))}
|
|
261
|
+
</View>
|
|
262
|
+
</>
|
|
263
|
+
) : null}
|
|
264
|
+
|
|
265
|
+
{show("zoom") && caps?.zoom.supported ? (
|
|
266
|
+
<ControlRow label={copy.zoom}>
|
|
267
|
+
<View style={uiStyles.presetRow}>
|
|
268
|
+
{ZOOM_PRESETS.filter(
|
|
269
|
+
(preset) => preset >= caps.zoom.min && preset <= caps.zoom.max,
|
|
270
|
+
).map((preset) => (
|
|
271
|
+
<PresetButton
|
|
272
|
+
key={preset}
|
|
273
|
+
label={`${preset}x`}
|
|
274
|
+
active={zoom === preset}
|
|
275
|
+
onPress={() => {
|
|
276
|
+
setZoomValue(preset);
|
|
277
|
+
void setZoom(preset);
|
|
278
|
+
}}
|
|
279
|
+
/>
|
|
280
|
+
))}
|
|
281
|
+
</View>
|
|
282
|
+
</ControlRow>
|
|
283
|
+
) : null}
|
|
284
|
+
|
|
285
|
+
{show("exposure") && caps?.exposureBias.supported ? (
|
|
286
|
+
<ControlRow label={copy.ev}>
|
|
287
|
+
<View style={uiStyles.presetRow}>
|
|
288
|
+
{EV_PRESETS.filter(
|
|
289
|
+
(preset) =>
|
|
290
|
+
preset >= caps.exposureBias.min && preset <= caps.exposureBias.max,
|
|
291
|
+
).map((preset) => (
|
|
292
|
+
<PresetButton
|
|
293
|
+
key={preset}
|
|
294
|
+
label={preset > 0 ? `+${preset}` : `${preset}`}
|
|
295
|
+
active={ev === preset}
|
|
296
|
+
onPress={() => {
|
|
297
|
+
setEvValue(preset);
|
|
298
|
+
void setExposureBias(preset);
|
|
299
|
+
}}
|
|
300
|
+
/>
|
|
301
|
+
))}
|
|
302
|
+
</View>
|
|
303
|
+
</ControlRow>
|
|
304
|
+
) : null}
|
|
305
|
+
|
|
306
|
+
{show("locks") && caps?.focusLock.supported ? (
|
|
307
|
+
<ControlRow label={copy.focusLock}>
|
|
308
|
+
<Switch
|
|
309
|
+
value={focusLocked}
|
|
310
|
+
accessibilityLabel={copy.focusLock}
|
|
311
|
+
onValueChange={(value) => {
|
|
312
|
+
setFocusLockedValue(value);
|
|
313
|
+
void setFocusLocked(value);
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
</ControlRow>
|
|
317
|
+
) : null}
|
|
318
|
+
{show("locks") && caps?.exposureLock.supported ? (
|
|
319
|
+
<ControlRow label={copy.exposureLock}>
|
|
320
|
+
<Switch
|
|
321
|
+
value={exposureLocked}
|
|
322
|
+
accessibilityLabel={copy.exposureLock}
|
|
323
|
+
onValueChange={(value) => {
|
|
324
|
+
setExposureLockedValue(value);
|
|
325
|
+
void setExposureLocked(value);
|
|
326
|
+
}}
|
|
327
|
+
/>
|
|
328
|
+
</ControlRow>
|
|
329
|
+
) : null}
|
|
330
|
+
{show("torch") && caps?.torch.supported ? (
|
|
331
|
+
<ControlRow label={copy.torch}>
|
|
332
|
+
<Switch
|
|
333
|
+
value={torch}
|
|
334
|
+
accessibilityLabel={copy.torch}
|
|
335
|
+
onValueChange={(value) => {
|
|
336
|
+
setTorchValue(value);
|
|
337
|
+
void setTorch(value);
|
|
338
|
+
}}
|
|
339
|
+
/>
|
|
340
|
+
</ControlRow>
|
|
341
|
+
) : null}
|
|
342
|
+
|
|
343
|
+
{show("uplink") ? (
|
|
344
|
+
<>
|
|
345
|
+
<ControlRow label={copy.network} />
|
|
346
|
+
<Pressable
|
|
347
|
+
accessibilityRole="button"
|
|
348
|
+
disabled={net.running || netBusy}
|
|
349
|
+
onPress={() => void runNetworkCheck()}
|
|
350
|
+
style={[uiStyles.actionButton, (net.running || netBusy) && uiStyles.disabled]}
|
|
351
|
+
>
|
|
352
|
+
<Text style={uiStyles.actionButtonText}>
|
|
353
|
+
{net.running ? copy.running : copy.runCheck}
|
|
354
|
+
</Text>
|
|
355
|
+
</Pressable>
|
|
356
|
+
{netBusy ? <Note>{copy.busyNote}</Note> : null}
|
|
357
|
+
{net.result ? (
|
|
358
|
+
<Text
|
|
359
|
+
style={[
|
|
360
|
+
styles.netResult,
|
|
361
|
+
net.result.verdict === "good"
|
|
362
|
+
? styles.netGood
|
|
363
|
+
: net.result.verdict === "ok"
|
|
364
|
+
? styles.netOk
|
|
365
|
+
: styles.netLow,
|
|
366
|
+
]}
|
|
367
|
+
>
|
|
368
|
+
{copy.resultLine({
|
|
369
|
+
uploadMbps: net.result.uploadMbps,
|
|
370
|
+
requiredMbps: net.result.requiredMbps,
|
|
371
|
+
resolution: config.resolution,
|
|
372
|
+
fps: config.fps,
|
|
373
|
+
pingMs: net.result.pingMs,
|
|
374
|
+
})}
|
|
375
|
+
{"\n"}
|
|
376
|
+
{net.result.verdict === "good"
|
|
377
|
+
? copy.good
|
|
378
|
+
: net.result.verdict === "ok"
|
|
379
|
+
? copy.ok
|
|
380
|
+
: copy.low}
|
|
381
|
+
</Text>
|
|
382
|
+
) : null}
|
|
383
|
+
{net.error ? <Note>{net.error}</Note> : null}
|
|
384
|
+
</>
|
|
385
|
+
) : null}
|
|
386
|
+
|
|
387
|
+
{footer}
|
|
388
|
+
</ScrollView>
|
|
389
|
+
</SafeAreaView>
|
|
390
|
+
</View>
|
|
391
|
+
</Modal>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const styles = StyleSheet.create({
|
|
396
|
+
overlaySide: { flex: 1, flexDirection: "row" },
|
|
397
|
+
overlayBottom: { flex: 1, flexDirection: "column" },
|
|
398
|
+
// Phần còn lại: mờ NHẸ để vẫn canh được khung hình trong lúc chỉnh; bấm là đóng.
|
|
399
|
+
backdrop: { flex: 1, backgroundColor: "rgba(0,0,0,0.3)" },
|
|
400
|
+
// Màn ngang: panel NỬA PHẢI — label và nút đứng gần nhau, nửa trái vẫn thấy
|
|
401
|
+
// khung hình. minWidth giữ dùng được trên màn hẹp, maxWidth để tablet không
|
|
402
|
+
// thành tấm bảng quá khổ.
|
|
403
|
+
panelSide: {
|
|
404
|
+
width: "50%",
|
|
405
|
+
minWidth: 340,
|
|
406
|
+
maxWidth: 480,
|
|
407
|
+
backgroundColor: uiColors.panel,
|
|
408
|
+
borderLeftWidth: 1,
|
|
409
|
+
borderLeftColor: uiColors.hairline,
|
|
410
|
+
},
|
|
411
|
+
// Màn dọc: bottom-sheet tối đa 70% chiều cao.
|
|
412
|
+
panelBottom: {
|
|
413
|
+
maxHeight: "70%",
|
|
414
|
+
backgroundColor: uiColors.panel,
|
|
415
|
+
borderTopLeftRadius: 18,
|
|
416
|
+
borderTopRightRadius: 18,
|
|
417
|
+
borderTopWidth: 1,
|
|
418
|
+
borderTopColor: uiColors.hairline,
|
|
419
|
+
},
|
|
420
|
+
panelHeader: {
|
|
421
|
+
flexDirection: "row",
|
|
422
|
+
alignItems: "center",
|
|
423
|
+
justifyContent: "space-between",
|
|
424
|
+
paddingHorizontal: 14,
|
|
425
|
+
paddingVertical: 10,
|
|
426
|
+
borderBottomWidth: 1,
|
|
427
|
+
borderBottomColor: uiColors.hairline,
|
|
428
|
+
},
|
|
429
|
+
closeX: {
|
|
430
|
+
width: 30,
|
|
431
|
+
height: 30,
|
|
432
|
+
borderRadius: 999,
|
|
433
|
+
backgroundColor: "rgba(255,255,255,0.1)",
|
|
434
|
+
alignItems: "center",
|
|
435
|
+
justifyContent: "center",
|
|
436
|
+
},
|
|
437
|
+
closeXText: { color: uiColors.textSecondary, fontSize: 14, fontWeight: "700" },
|
|
438
|
+
content: { padding: 14, paddingBottom: 24, gap: 10 },
|
|
439
|
+
heading: { color: uiColors.text, fontSize: 15, fontWeight: "800" },
|
|
440
|
+
netResult: { fontSize: 12, fontWeight: "700", lineHeight: 17 },
|
|
441
|
+
netGood: { color: uiColors.good },
|
|
442
|
+
netOk: { color: uiColors.ok },
|
|
443
|
+
netLow: { color: uiColors.low },
|
|
444
|
+
cameraList: { gap: 5 },
|
|
445
|
+
});
|