feat: Support remote input

This commit is contained in:
zimplexing
2025-07-10 22:18:34 +08:00
parent eaa783824d
commit 9e4d4ca242
12 changed files with 9691 additions and 9 deletions

View File

@@ -7,6 +7,8 @@ import { Platform } from "react-native";
import Toast from "react-native-toast-message";
import { useSettingsStore } from "@/stores/settingsStore";
import { useRemoteControlStore } from "@/stores/remoteControlStore";
import { remoteControlService } from "@/services/remoteControlService";
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
@@ -31,6 +33,23 @@ export default function RootLayout() {
}
}, [loaded, error]);
useEffect(() => {
// Initialize the service with store actions to break require cycle
const { setMessage, hideModal } = useRemoteControlStore.getState();
remoteControlService.init({
onMessage: setMessage,
onHandshake: hideModal,
});
// Start the remote control server on app launch
useRemoteControlStore.getState().startServer();
return () => {
// Stop the server on app close
useRemoteControlStore.getState().stopServer();
};
}, []);
if (!loaded && !error) {
return null;
}

View File

@@ -6,7 +6,7 @@ import { ThemedView } from "@/components/ThemedView";
import { StyledButton } from "@/components/StyledButton";
import { AVPlaybackStatus } from "expo-av";
const M3U_URL = "https://raw.githubusercontent.com/fanmingming/live/refs/heads/main/tv/m3u/ipv6.m3u";
const M3U_URL = "https://raw.githubusercontent.com/sjnhnp/adblock/refs/heads/main/filtered_http_only_valid.m3u";
export default function LiveScreen() {
const [channels, setChannels] = useState<Channel[]>([]);

View File

@@ -4,8 +4,10 @@ import { ThemedView } from "@/components/ThemedView";
import { ThemedText } from "@/components/ThemedText";
import VideoCard from "@/components/VideoCard.tv";
import { api, SearchResult } from "@/services/api";
import { Search } from "lucide-react-native";
import { Search, QrCode } from "lucide-react-native";
import { StyledButton } from "@/components/StyledButton";
import { useRemoteControlStore } from "@/stores/remoteControlStore";
import { RemoteControlModal } from "@/components/RemoteControlModal";
export default function SearchScreen() {
const [keyword, setKeyword] = useState("");
@@ -15,6 +17,15 @@ export default function SearchScreen() {
const textInputRef = useRef<TextInput>(null);
const colorScheme = "dark"; // Replace with useColorScheme() if needed
const [isInputFocused, setIsInputFocused] = useState(false);
const { showModal: showRemoteModal, lastMessage } = useRemoteControlStore();
useEffect(() => {
if (lastMessage) {
const realMessage = lastMessage.split("_")[0];
setKeyword(realMessage);
handleSearch(realMessage);
}
}, [lastMessage]);
useEffect(() => {
// Focus the text input when the screen loads
@@ -24,8 +35,9 @@ export default function SearchScreen() {
return () => clearTimeout(timer);
}, []);
const handleSearch = async () => {
if (!keyword.trim()) {
const handleSearch = async (searchText?: string) => {
const term = typeof searchText === "string" ? searchText : keyword;
if (!term.trim()) {
Keyboard.dismiss();
return;
}
@@ -33,7 +45,7 @@ export default function SearchScreen() {
setLoading(true);
setError(null);
try {
const response = await api.searchVideos(keyword);
const response = await api.searchVideos(term);
if (response.results.length > 0) {
setResults(response.results);
} else {
@@ -47,6 +59,8 @@ export default function SearchScreen() {
}
};
const onSearchPress = () => handleSearch();
const renderItem = ({ item }: { item: SearchResult }) => (
<VideoCard
id={item.id.toString()}
@@ -78,12 +92,15 @@ export default function SearchScreen() {
onChangeText={setKeyword}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
onSubmitEditing={handleSearch} // Allow searching with remote 'enter' button
onSubmitEditing={onSearchPress}
returnKeyType="search"
/>
<StyledButton style={styles.searchButton} onPress={handleSearch}>
<StyledButton style={styles.searchButton} onPress={onSearchPress}>
<Search size={24} color={colorScheme === "dark" ? "white" : "black"} />
</StyledButton>
<StyledButton style={styles.qrButton} onPress={showRemoteModal}>
<QrCode size={24} color={colorScheme === "dark" ? "white" : "black"} />
</StyledButton>
</View>
{loading ? (
@@ -108,6 +125,7 @@ export default function SearchScreen() {
}
/>
)}
<RemoteControlModal />
</ThemedView>
);
}
@@ -140,6 +158,11 @@ const styles = StyleSheet.create({
// backgroundColor is now set dynamically
borderRadius: 8,
},
qrButton: {
padding: 12,
borderRadius: 8,
marginLeft: 10,
},
centerContainer: {
flex: 1,
justifyContent: "center",

View File

@@ -0,0 +1,82 @@
import React from "react";
import { Modal, View, Text, StyleSheet } from "react-native";
import QRCode from "react-native-qrcode-svg";
import { useRemoteControlStore } from "@/stores/remoteControlStore";
import { ThemedView } from "./ThemedView";
import { ThemedText } from "./ThemedText";
import { StyledButton } from "./StyledButton";
export const RemoteControlModal: React.FC = () => {
const { isModalVisible, hideModal, serverUrl, error } = useRemoteControlStore();
return (
<Modal animationType="fade" transparent={true} visible={isModalVisible} onRequestClose={hideModal}>
<View style={styles.modalContainer}>
<ThemedView style={styles.modalContent}>
<ThemedText style={styles.title}></ThemedText>
<View style={styles.qrContainer}>
{serverUrl ? (
<>
<QRCode value={serverUrl} size={200} backgroundColor="white" color="black" />
</>
) : (
<ThemedText style={styles.statusText}>{error ? `错误: ${error}` : "正在生成二维码..."}</ThemedText>
)}
</View>
<ThemedText style={styles.instructions}>
使 TV 访{serverUrl}
</ThemedText>
<StyledButton text="关闭" onPress={hideModal} style={styles.button} variant="primary" />
</ThemedView>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0, 0, 0, 0.7)",
},
modalContent: {
width: "85%",
maxWidth: 400,
padding: 24,
borderRadius: 12,
alignItems: "center",
},
title: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 10,
paddingTop: 10,
},
qrContainer: {
width: 220,
height: 220,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#f0f0f0",
borderRadius: 8,
marginBottom: 20,
},
statusText: {
textAlign: "center",
fontSize: 16,
},
serverUrlText: {
marginTop: 10,
fontSize: 12,
},
instructions: {
textAlign: "center",
marginBottom: 24,
fontSize: 16,
color: "#ccc",
},
button: {
width: "100%",
},
});

View File

@@ -0,0 +1,136 @@
# 手机遥控功能实现方案 (V2)
本文档详细描述了在 OrionTV 应用中集成一个基于 **HTTP 请求** 的手机遥控功能的完整方案。
---
## 1. 核心功能与流程
该功能允许用户通过手机浏览器向 TV 端发送文本消息TV 端接收后以 Toast 形式进行展示。服务将在应用启动时自动开启,用户可在设置中找到入口以显示连接二维码。
### 流程图
```mermaid
sequenceDiagram
participant App as App 启动
participant RemoteControlStore as 状态管理 (TV)
participant RemoteControlService as 遥控服务 (TV)
participant User as 用户
participant SettingsUI as 设置界面 (TV)
participant PhoneBrowser as 手机浏览器
App->>RemoteControlStore: App 启动, 触发 startHttpServer()
RemoteControlStore->>RemoteControlService: 启动 HTTP 服务
RemoteControlService-->>RemoteControlStore: 更新服务状态 (IP, Port)
User->>SettingsUI: 打开设置, 点击“手机遥控”按钮
SettingsUI->>RemoteControlStore: 获取服务 URL
RemoteControlStore-->>SettingsUI: 返回 serverUrl
SettingsUI-->>User: 显示二维码弹窗
User->>PhoneBrowser: 扫描二维码
PhoneBrowser->>RemoteControlService: (HTTP GET) 请求网页
RemoteControlService-->>PhoneBrowser: 返回 HTML 页面
User->>PhoneBrowser: 输入文本并发送
PhoneBrowser->>RemoteControlService: (HTTP POST /message) 发送消息
RemoteControlService->>RemoteControlStore: 处理消息 (显示 Toast)
```
---
## 2. 技术选型
* **HTTP 服务**: `react-native-http-bridge`
* **二维码生成**: `react-native-qrcode-svg`
* **网络信息 (IP 地址)**: `@react-native-community/netinfo`
* **状态管理**: `zustand` (项目已集成)
---
## 3. 项目结构变更
### 新增文件
* `services/remoteControlService.ts`: 封装 HTTP 服务的核心逻辑。
* `stores/remoteControlStore.ts`: 使用 Zustand 管理远程控制服务的状态。
* `components/RemoteControlModal.tsx`: 显示二维码和连接信息的弹窗组件。
* `types/react-native-http-bridge.d.ts`: `react-native-http-bridge` 的 TypeScript 类型定义。
### 修改文件
* `app/_layout.tsx`: 在应用根组件中调用服务启动逻辑。
* `components/SettingsModal.tsx`: 添加“手机遥控”按钮,用于触发二维码弹窗。
* `package.json`: 添加新依赖。
---
## 4. 实现细节
### a. 状态管理 (`stores/remoteControlStore.ts`)
创建一个 Zustand store 来管理遥控服务的状态。
* **State**:
* `isServerRunning`: `boolean` - 服务是否正在运行。
* `serverUrl`: `string | null` - 完整的 HTTP 服务 URL (e.g., `http://192.168.1.5:12346`)。
* `error`: `string | null` - 错误信息。
* **Actions**:
* `startServer()`: 异步 action调用 `remoteControlService.startServer` 并更新 state。
* `stopServer()`: 调用 `remoteControlService.stopServer` 并更新 state。
### b. 服务层 (`services/remoteControlService.ts`)
实现服务的启动、停止和消息处理。
* **`startServer()`**:
1. 使用 `@react-native-community/netinfo` 获取 IP 地址。
2. 定义一个包含 `fetch` API 调用逻辑的 HTML 字符串。
3. 使用 `react-native-http-bridge` 在固定端口(如 `12346`)启动 HTTP 服务。
4. 配置 `GET /` 路由以返回 HTML 页面。
5. 配置 `POST /message` 路由来接收手机端发送的消息,并使用 `Toast` 显示。
6. 返回服务器 URL。
* **`stopServer()`**:
1. 调用 `httpBridge.stop()`
### c. UI 集成
* **`app/_layout.tsx`**:
* 在根组件 `useEffect` 中调用 `useRemoteControlStore.getState().startServer()`,实现服务自启。
* **`components/SettingsModal.tsx`**:
* 添加一个 `<StyledButton text="手机遥控" />`
* 点击按钮时,触发 `RemoteControlModal` 的显示。
* **`components/RemoteControlModal.tsx`**:
*`remoteControlStore` 中获取 `serverUrl`
* 如果 `serverUrl` 存在,则使用 `react-native-qrcode-svg``<QRCode />` 组件显示二维码。
* 如果不存在,则显示加载中或错误信息。
### d. 网页内容 (HTML)
一个简单的 HTML 页面,包含一个输入框和一个按钮。
```html
<html>
<head>
<title>OrionTV Remote</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style> /* ... some basic styles ... */ </style>
</head>
<body>
<h3>发送消息到 TV</h3>
<input id="text" />
<button onclick="send()">发送</button>
<script>
function send() {
const val = document.getElementById("text").value;
if (val) {
fetch("/message", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: val })
});
document.getElementById("text").value = '';
}
}
</script>
</body>
</html>

View File

@@ -4,6 +4,7 @@
"main": "expo-router/entry",
"version": "1.1.1",
"scripts": {
"postinstall": "node ./scripts/patch-http-bridge.js",
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
"start-tv": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
"android": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:android",
@@ -26,8 +27,10 @@
"react-native": "npm:react-native-tvos@~0.74.2-0"
},
"dependencies": {
"react-native-http-bridge": "^0.4.0",
"@expo/vector-icons": "^14.0.0",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/netinfo": "^11.3.2",
"@react-navigation/native": "^6.0.2",
"expo": "~51.0.13",
"expo-av": "~14.0.7",
@@ -46,6 +49,7 @@
"react-native": "npm:react-native-tvos@~0.74.2-0",
"react-native-gesture-handler": "~2.16.1",
"react-native-media-console": "*",
"react-native-qrcode-svg": "^6.3.1",
"react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "3.31.1",

View File

@@ -0,0 +1,82 @@
const fs = require('fs');
const path = require('path');
function patchFile(filePath, patches) {
if (!fs.existsSync(filePath)) {
console.log(`File not found, skipping patch: ${filePath}`);
return;
}
console.log(`Patching ${filePath}...`);
let content = fs.readFileSync(filePath, 'utf8');
patches.forEach(patch => {
content = content.replace(patch.find, patch.replace);
});
fs.writeFileSync(filePath, content, 'utf8');
}
// --- Patch build.gradle ---
const gradleFile = path.resolve(__dirname, '..', 'node_modules', 'react-native-http-bridge', 'android', 'build.gradle');
patchFile(gradleFile, [
{
find: /jcenter\(\)/g,
replace: 'google()\n mavenCentral()'
},
{
find: "classpath 'com.android.tools.build:gradle:2.2.0'",
replace: "classpath 'com.android.tools.build:gradle:7.3.1'"
},
{
find: 'compileSdkVersion 23',
replace: 'compileSdkVersion 33'
},
{
find: 'buildToolsVersion "23.0.1"',
replace: 'buildToolsVersion "33.0.0"'
},
{
find: /compile /g,
replace: 'implementation '
},
{
find: /android {/,
replace: 'android {\n namespace "me.alwx.HttpServer"'
}
]);
// --- Patch AndroidManifest.xml ---
const manifestFile = path.resolve(__dirname, '..', 'node_modules', 'react-native-http-bridge', 'android', 'src', 'main', 'AndroidManifest.xml');
patchFile(manifestFile, [
{
find: /package="me.alwx.HttpServer"/,
replace: ''
}
]);
// --- Patch Server.java ---
const serverJavaFile = path.resolve(__dirname, '..', 'node_modules', 'react-native-http-bridge', 'android', 'src', 'main', 'java', 'me', 'alwx', 'HttpServer', 'Server.java');
patchFile(serverJavaFile, [
{
find: 'import android.support.annotation.Nullable;',
replace: 'import androidx.annotation.Nullable;'
}
]);
// --- Patch HttpServerReactPackage.java ---
const packageJavaFile = path.resolve(__dirname, '..', 'node_modules', 'react-native-http-bridge', 'android', 'src', 'main', 'java', 'me', 'alwx', 'HttpServer', 'HttpServerReactPackage.java');
patchFile(packageJavaFile, [
{
find: '@Override\n public List<Class<? extends JavaScriptModule>> createJSModules()',
replace: 'public List<Class<? extends JavaScriptModule>> createJSModules()'
}
]);
// --- Patch HttpServerModule.java for better logging ---
const moduleJavaFile = path.resolve(__dirname, '..', 'node_modules', 'react-native-http-bridge', 'android', 'src', 'main', 'java', 'me', 'alwx', 'HttpServer', 'HttpServerModule.java');
patchFile(moduleJavaFile, [
{
find: 'Log.e(MODULE_NAME, e.getMessage());',
replace: 'Log.e(MODULE_NAME, "Failed to start server", e);'
}
]);
console.log('Finished patching react-native-http-bridge.');

View File

@@ -0,0 +1,132 @@
import httpBridge from 'react-native-http-bridge';
import NetInfo from '@react-native-community/netinfo';
const PORT = 12346;
const getRemotePageHTML = () => {
return `
<!DOCTYPE html>
<html>
<head>
<title>OrionTV Remote</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; background-color: #121212; color: white; }
h3 { color: #eee; }
#container { display: flex; flex-direction: column; align-items: center; width: 90%; max-width: 400px; }
#text { width: 100%; padding: 15px; font-size: 16px; border-radius: 8px; border: 1px solid #333; background-color: #2a2a2a; color: white; margin-bottom: 20px; box-sizing: border-box; }
button { width: 100%; padding: 15px; font-size: 18px; font-weight: bold; border: none; border-radius: 8px; background-color: #007AFF; color: white; cursor: pointer; }
button:active { background-color: #0056b3; }
</style>
</head>
<body>
<div id="container">
<h3>Send a message to TV</h3>
<input id="text" placeholder="Type here..." />
<button onclick="send()">Send</button>
</div>
<script>
window.addEventListener('DOMContentLoaded', () => {
fetch('/handshake', { method: 'POST' }).catch(console.error);
});
function send() {
const input = document.getElementById("text");
const value = input.value;
if (value) {
fetch("/message", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: value })
})
.catch(err => console.error(err));
input.value = '';
}
}
</script>
</body>
</html>
`;
};
class RemoteControlService {
private isRunning = false;
private onMessage: (message: string) => void = () => {};
private onHandshake: () => void = () => {};
public init(actions: { onMessage: (message: string) => void; onHandshake: () => void }) {
this.onMessage = actions.onMessage;
this.onHandshake = actions.onHandshake;
}
public async startServer(): Promise<string> {
console.log('[RemoteControl] Attempting to start server...');
if (this.isRunning) {
console.log('[RemoteControl] Server is already running.');
throw new Error('Server is already running.');
}
const netState = await NetInfo.fetch();
console.log('[RemoteControl] NetInfo state:', JSON.stringify(netState, null, 2));
let ipAddress: string | null = null;
if (netState.type === 'wifi' || netState.type === 'ethernet') {
ipAddress = (netState.details as any)?.ipAddress ?? null;
}
if (!ipAddress) {
console.error('[RemoteControl] Could not get IP address.');
throw new Error('无法获取IP地址请确认设备已连接到WiFi或以太网。');
}
console.log(`[RemoteControl] Got IP address: ${ipAddress}`);
try {
// The third argument to start() is the request handler, not a startup callback.
httpBridge.start(
PORT,
'OrionTVRemoteService',
(request: { url: string; type: string; requestId: string; postData: string }) => {
const { url, type: method, requestId, postData: body } = request;
if (method === 'GET' && url === '/') {
const html = getRemotePageHTML();
httpBridge.respond(requestId, 200, 'text/html', html);
} else if (method === 'POST' && url === '/message') {
try {
const parsedBody = JSON.parse(body);
const message = parsedBody.message;
if (message) {
this.onMessage(message);
}
httpBridge.respond(requestId, 200, 'application/json', JSON.stringify({ status: 'ok' }));
} catch (e) {
httpBridge.respond(requestId, 400, 'application/json', JSON.stringify({ error: 'Bad Request' }));
}
} else if (method === 'POST' && url === '/handshake') {
this.onHandshake();
httpBridge.respond(requestId, 200, 'application/json', JSON.stringify({ status: 'ok' }));
} else {
httpBridge.respond(requestId, 404, 'text/plain', 'Not Found');
}
}
);
console.log('[RemoteControl] http-bridge start command issued.');
this.isRunning = true;
const url = `http://${ipAddress}:${PORT}`;
console.log(`[RemoteControl] Server should be running at: ${url}`);
return url;
} catch (error) {
console.error('[RemoteControl] Failed to issue start command to http-bridge.', error);
this.isRunning = false;
throw new Error(error instanceof Error ? error.message : 'Failed to start server');
}
}
public stopServer() {
if (this.isRunning) {
httpBridge.stop();
this.isRunning = false;
}
}
}
export const remoteControlService = new RemoteControlService();

View File

@@ -0,0 +1,52 @@
import { create } from 'zustand';
import { remoteControlService } from '@/services/remoteControlService';
interface RemoteControlState {
isServerRunning: boolean;
serverUrl: string | null;
error: string | null;
startServer: () => Promise<void>;
stopServer: () => void;
isModalVisible: boolean;
showModal: () => void;
hideModal: () => void;
lastMessage: string | null;
setMessage: (message: string) => void;
}
export const useRemoteControlStore = create<RemoteControlState>((set, get) => ({
isServerRunning: false,
serverUrl: null,
error: null,
isModalVisible: false,
lastMessage: null,
startServer: async () => {
if (get().isServerRunning) {
return;
}
try {
const url = await remoteControlService.startServer();
console.log(`[RemoteControlStore] Server started, URL: ${url}`);
set({ isServerRunning: true, serverUrl: url, error: null });
} catch (e: any) {
const errorMessage = e.message || 'Failed to start server';
console.error('[RemoteControlStore] Failed to start server:', errorMessage);
set({ error: errorMessage });
}
},
stopServer: () => {
if (get().isServerRunning) {
remoteControlService.stopServer();
set({ isServerRunning: false, serverUrl: null });
}
},
showModal: () => set({ isModalVisible: true }),
hideModal: () => set({ isModalVisible: false }),
setMessage: (message: string) => {
set({ lastMessage: `${message}_${Date.now()}` });
},
}));

13
types/react-native-http-bridge.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
declare module 'react-native-http-bridge' {
import { EmitterSubscription } from 'react-native';
interface HttpBridge {
start(port: number, serviceName: string, callback: (request: { url: string; type: string; requestId: string; postData: string }) => void): void;
stop(): void;
on(event: 'request', callback: (request: any) => void): EmitterSubscription;
respond(requestId: string, code: number, type: string, body: string): void;
}
const httpBridge: HttpBridge;
export default httpBridge;
}

9096
yarn-error.log Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1670,6 +1670,11 @@
prompts "^2.4.2"
semver "^7.5.2"
"@react-native-community/netinfo@^11.3.2":
version "11.4.1"
resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-11.4.1.tgz#a3c247aceab35f75dd0aa4bfa85d2be5a4508688"
integrity sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==
"@react-native-tvos/config-tv@^0.0.10":
version "0.0.10"
resolved "https://registry.yarnpkg.com/@react-native-tvos/config-tv/-/config-tv-0.0.10.tgz#38fe1571e24c6790b43137d130832c68b366c295"
@@ -3549,6 +3554,11 @@ diff-sequences@^29.6.3:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==
dijkstrajs@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23"
integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -6830,6 +6840,11 @@ pngjs@^3.3.0:
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
pngjs@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
possible-typed-array-names@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae"
@@ -6920,7 +6935,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2, prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.7.2:
prop-types@^15.7.2, prop-types@^15.8.0:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -6959,6 +6974,15 @@ qrcode-terminal@0.11.0:
resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz#ffc6c28a2fc0bfb47052b47e23f4f446a5fbdb9e"
integrity sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==
qrcode@^1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88"
integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==
dependencies:
dijkstrajs "^1.0.1"
pngjs "^5.0.0"
yargs "^15.3.1"
query-string@^7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328"
@@ -7067,11 +7091,25 @@ react-native-helmet-async@2.0.4:
react-fast-compare "^3.2.2"
shallowequal "^1.1.0"
react-native-http-bridge@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/react-native-http-bridge/-/react-native-http-bridge-0.4.1.tgz#d120a25b23fb98ec708def75fef5489d79173e81"
integrity sha512-a2NSuLMh2vxwr1aqmrHGZoXkRM78YXTzX5i9AffkGXLLL3ssLLpDYgsU3HD9S+aaJu+jbDjoxwuZKYuIKJPznw==
react-native-media-console@*:
version "2.2.4"
resolved "https://registry.yarnpkg.com/react-native-media-console/-/react-native-media-console-2.2.4.tgz#76a232cdcb645cfdb25bacddee514f360eb4947d"
integrity sha512-CpOunVkGkMRg7DoGYlXfGITbSuqvCId7CFcWyDED3CjJ1CCym4dB670GiBNymH/Sh5p1AaW8kvHng7PZ7fYUBQ==
react-native-qrcode-svg@^6.3.1:
version "6.3.15"
resolved "https://registry.yarnpkg.com/react-native-qrcode-svg/-/react-native-qrcode-svg-6.3.15.tgz#20d7a189dff5b7ee8a75222a1268a805497cac75"
integrity sha512-vLuNImGfstE8u+rlF4JfFpq65nPhmByuDG6XUPWh8yp8MgLQX11rN5eQ8nb/bf4OB+V8XoLTJB/AZF2g7jQSSQ==
dependencies:
prop-types "^15.8.0"
qrcode "^1.5.4"
text-encoding "^0.7.0"
react-native-reanimated@~3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.10.1.tgz#3c37d1100bbba0065df39c96aab0c1ff1b50c0fa"
@@ -8172,6 +8210,11 @@ test-exclude@^6.0.0:
glob "^7.1.4"
minimatch "^3.0.4"
text-encoding@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.7.0.tgz#f895e836e45990624086601798ea98e8f36ee643"
integrity sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -8889,7 +8932,7 @@ yargs-parser@^21.1.1:
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs@^15.1.0:
yargs@^15.1.0, yargs@^15.3.1:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==