mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-06-12 08:53:10 +08:00
feat: Support remote input
This commit is contained in:
@@ -7,6 +7,8 @@ import { Platform } from "react-native";
|
|||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
|
|
||||||
import { useSettingsStore } from "@/stores/settingsStore";
|
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.
|
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||||
SplashScreen.preventAutoHideAsync();
|
SplashScreen.preventAutoHideAsync();
|
||||||
@@ -31,6 +33,23 @@ export default function RootLayout() {
|
|||||||
}
|
}
|
||||||
}, [loaded, error]);
|
}, [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) {
|
if (!loaded && !error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ThemedView } from "@/components/ThemedView";
|
|||||||
import { StyledButton } from "@/components/StyledButton";
|
import { StyledButton } from "@/components/StyledButton";
|
||||||
import { AVPlaybackStatus } from "expo-av";
|
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() {
|
export default function LiveScreen() {
|
||||||
const [channels, setChannels] = useState<Channel[]>([]);
|
const [channels, setChannels] = useState<Channel[]>([]);
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { ThemedView } from "@/components/ThemedView";
|
|||||||
import { ThemedText } from "@/components/ThemedText";
|
import { ThemedText } from "@/components/ThemedText";
|
||||||
import VideoCard from "@/components/VideoCard.tv";
|
import VideoCard from "@/components/VideoCard.tv";
|
||||||
import { api, SearchResult } from "@/services/api";
|
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 { StyledButton } from "@/components/StyledButton";
|
||||||
|
import { useRemoteControlStore } from "@/stores/remoteControlStore";
|
||||||
|
import { RemoteControlModal } from "@/components/RemoteControlModal";
|
||||||
|
|
||||||
export default function SearchScreen() {
|
export default function SearchScreen() {
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
@@ -15,6 +17,15 @@ export default function SearchScreen() {
|
|||||||
const textInputRef = useRef<TextInput>(null);
|
const textInputRef = useRef<TextInput>(null);
|
||||||
const colorScheme = "dark"; // Replace with useColorScheme() if needed
|
const colorScheme = "dark"; // Replace with useColorScheme() if needed
|
||||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||||
|
const { showModal: showRemoteModal, lastMessage } = useRemoteControlStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastMessage) {
|
||||||
|
const realMessage = lastMessage.split("_")[0];
|
||||||
|
setKeyword(realMessage);
|
||||||
|
handleSearch(realMessage);
|
||||||
|
}
|
||||||
|
}, [lastMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Focus the text input when the screen loads
|
// Focus the text input when the screen loads
|
||||||
@@ -24,8 +35,9 @@ export default function SearchScreen() {
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSearch = async () => {
|
const handleSearch = async (searchText?: string) => {
|
||||||
if (!keyword.trim()) {
|
const term = typeof searchText === "string" ? searchText : keyword;
|
||||||
|
if (!term.trim()) {
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -33,7 +45,7 @@ export default function SearchScreen() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const response = await api.searchVideos(keyword);
|
const response = await api.searchVideos(term);
|
||||||
if (response.results.length > 0) {
|
if (response.results.length > 0) {
|
||||||
setResults(response.results);
|
setResults(response.results);
|
||||||
} else {
|
} else {
|
||||||
@@ -47,6 +59,8 @@ export default function SearchScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSearchPress = () => handleSearch();
|
||||||
|
|
||||||
const renderItem = ({ item }: { item: SearchResult }) => (
|
const renderItem = ({ item }: { item: SearchResult }) => (
|
||||||
<VideoCard
|
<VideoCard
|
||||||
id={item.id.toString()}
|
id={item.id.toString()}
|
||||||
@@ -78,12 +92,15 @@ export default function SearchScreen() {
|
|||||||
onChangeText={setKeyword}
|
onChangeText={setKeyword}
|
||||||
onFocus={() => setIsInputFocused(true)}
|
onFocus={() => setIsInputFocused(true)}
|
||||||
onBlur={() => setIsInputFocused(false)}
|
onBlur={() => setIsInputFocused(false)}
|
||||||
onSubmitEditing={handleSearch} // Allow searching with remote 'enter' button
|
onSubmitEditing={onSearchPress}
|
||||||
returnKeyType="search"
|
returnKeyType="search"
|
||||||
/>
|
/>
|
||||||
<StyledButton style={styles.searchButton} onPress={handleSearch}>
|
<StyledButton style={styles.searchButton} onPress={onSearchPress}>
|
||||||
<Search size={24} color={colorScheme === "dark" ? "white" : "black"} />
|
<Search size={24} color={colorScheme === "dark" ? "white" : "black"} />
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
|
<StyledButton style={styles.qrButton} onPress={showRemoteModal}>
|
||||||
|
<QrCode size={24} color={colorScheme === "dark" ? "white" : "black"} />
|
||||||
|
</StyledButton>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -108,6 +125,7 @@ export default function SearchScreen() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<RemoteControlModal />
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -140,6 +158,11 @@ const styles = StyleSheet.create({
|
|||||||
// backgroundColor is now set dynamically
|
// backgroundColor is now set dynamically
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
|
qrButton: {
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
centerContainer: {
|
centerContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
|||||||
82
components/RemoteControlModal.tsx
Normal file
82
components/RemoteControlModal.tsx
Normal 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%",
|
||||||
|
},
|
||||||
|
});
|
||||||
136
docs/REMOTE_CONTROL_FEATURE.md
Normal file
136
docs/REMOTE_CONTROL_FEATURE.md
Normal 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>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"postinstall": "node ./scripts/patch-http-bridge.js",
|
||||||
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
||||||
"start-tv": "EXPO_TV=1 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",
|
"android": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:android",
|
||||||
@@ -26,8 +27,10 @@
|
|||||||
"react-native": "npm:react-native-tvos@~0.74.2-0"
|
"react-native": "npm:react-native-tvos@~0.74.2-0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"react-native-http-bridge": "^0.4.0",
|
||||||
"@expo/vector-icons": "^14.0.0",
|
"@expo/vector-icons": "^14.0.0",
|
||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
|
"@react-native-community/netinfo": "^11.3.2",
|
||||||
"@react-navigation/native": "^6.0.2",
|
"@react-navigation/native": "^6.0.2",
|
||||||
"expo": "~51.0.13",
|
"expo": "~51.0.13",
|
||||||
"expo-av": "~14.0.7",
|
"expo-av": "~14.0.7",
|
||||||
@@ -46,6 +49,7 @@
|
|||||||
"react-native": "npm:react-native-tvos@~0.74.2-0",
|
"react-native": "npm:react-native-tvos@~0.74.2-0",
|
||||||
"react-native-gesture-handler": "~2.16.1",
|
"react-native-gesture-handler": "~2.16.1",
|
||||||
"react-native-media-console": "*",
|
"react-native-media-console": "*",
|
||||||
|
"react-native-qrcode-svg": "^6.3.1",
|
||||||
"react-native-reanimated": "~3.10.1",
|
"react-native-reanimated": "~3.10.1",
|
||||||
"react-native-safe-area-context": "4.10.1",
|
"react-native-safe-area-context": "4.10.1",
|
||||||
"react-native-screens": "3.31.1",
|
"react-native-screens": "3.31.1",
|
||||||
|
|||||||
82
scripts/patch-http-bridge.js
Normal file
82
scripts/patch-http-bridge.js
Normal 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.');
|
||||||
132
services/remoteControlService.ts
Normal file
132
services/remoteControlService.ts
Normal 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();
|
||||||
52
stores/remoteControlStore.ts
Normal file
52
stores/remoteControlStore.ts
Normal 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
13
types/react-native-http-bridge.d.ts
vendored
Normal 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
9096
yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
47
yarn.lock
47
yarn.lock
@@ -1670,6 +1670,11 @@
|
|||||||
prompts "^2.4.2"
|
prompts "^2.4.2"
|
||||||
semver "^7.5.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":
|
"@react-native-tvos/config-tv@^0.0.10":
|
||||||
version "0.0.10"
|
version "0.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/@react-native-tvos/config-tv/-/config-tv-0.0.10.tgz#38fe1571e24c6790b43137d130832c68b366c295"
|
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"
|
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
|
||||||
integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==
|
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:
|
dir-glob@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
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"
|
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
|
||||||
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
|
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:
|
possible-typed-array-names@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae"
|
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"
|
kleur "^3.0.3"
|
||||||
sisteransi "^1.0.5"
|
sisteransi "^1.0.5"
|
||||||
|
|
||||||
prop-types@^15.7.2:
|
prop-types@^15.7.2, prop-types@^15.8.0:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
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"
|
resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz#ffc6c28a2fc0bfb47052b47e23f4f446a5fbdb9e"
|
||||||
integrity sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==
|
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:
|
query-string@^7.1.3:
|
||||||
version "7.1.3"
|
version "7.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328"
|
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"
|
react-fast-compare "^3.2.2"
|
||||||
shallowequal "^1.1.0"
|
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@*:
|
react-native-media-console@*:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-media-console/-/react-native-media-console-2.2.4.tgz#76a232cdcb645cfdb25bacddee514f360eb4947d"
|
resolved "https://registry.yarnpkg.com/react-native-media-console/-/react-native-media-console-2.2.4.tgz#76a232cdcb645cfdb25bacddee514f360eb4947d"
|
||||||
integrity sha512-CpOunVkGkMRg7DoGYlXfGITbSuqvCId7CFcWyDED3CjJ1CCym4dB670GiBNymH/Sh5p1AaW8kvHng7PZ7fYUBQ==
|
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:
|
react-native-reanimated@~3.10.1:
|
||||||
version "3.10.1"
|
version "3.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.10.1.tgz#3c37d1100bbba0065df39c96aab0c1ff1b50c0fa"
|
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"
|
glob "^7.1.4"
|
||||||
minimatch "^3.0.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:
|
text-table@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
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"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||||
|
|
||||||
yargs@^15.1.0:
|
yargs@^15.1.0, yargs@^15.3.1:
|
||||||
version "15.4.1"
|
version "15.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
||||||
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||||
|
|||||||
Reference in New Issue
Block a user