From 5f92f76f4b9cc4795e2bed79c6782545c58d20fa Mon Sep 17 00:00:00 2001 From: zimplexing Date: Fri, 11 Jul 2025 18:13:06 +0800 Subject: [PATCH] feat: Enable remote input functionality and enhance settings management for remote control --- app/search.tsx | 21 ++- app/settings.tsx | 37 ++++- components/settings/APIConfigSection.tsx | 138 ++++++++++++------- components/settings/LiveStreamSection.tsx | 160 +++++++++++++--------- services/storage.ts | 2 +- stores/remoteControlStore.ts | 10 ++ 6 files changed, 245 insertions(+), 123 deletions(-) diff --git a/app/search.tsx b/app/search.tsx index 9a69439..f7abf8c 100644 --- a/app/search.tsx +++ b/app/search.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from "react"; -import { View, TextInput, StyleSheet, FlatList, ActivityIndicator, Text, Keyboard } from "react-native"; +import { View, TextInput, StyleSheet, FlatList, ActivityIndicator, Alert, Keyboard } from "react-native"; import { ThemedView } from "@/components/ThemedView"; import { ThemedText } from "@/components/ThemedText"; import VideoCard from "@/components/VideoCard.tv"; @@ -8,6 +8,8 @@ import { Search, QrCode } from "lucide-react-native"; import { StyledButton } from "@/components/StyledButton"; import { useRemoteControlStore } from "@/stores/remoteControlStore"; import { RemoteControlModal } from "@/components/RemoteControlModal"; +import { useSettingsStore } from "@/stores/settingsStore"; +import { useRouter } from "expo-router"; export default function SearchScreen() { const [keyword, setKeyword] = useState(""); @@ -18,13 +20,17 @@ export default function SearchScreen() { const colorScheme = "dark"; // Replace with useColorScheme() if needed const [isInputFocused, setIsInputFocused] = useState(false); const { showModal: showRemoteModal, lastMessage } = useRemoteControlStore(); + const { remoteInputEnabled } = useSettingsStore(); + const router = useRouter(); useEffect(() => { if (lastMessage) { + console.log("Received remote input:", lastMessage); const realMessage = lastMessage.split("_")[0]; setKeyword(realMessage); handleSearch(realMessage); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [lastMessage]); useEffect(() => { @@ -61,6 +67,17 @@ export default function SearchScreen() { const onSearchPress = () => handleSearch(); + const handleQrPress = () => { + if (!remoteInputEnabled) { + Alert.alert("远程输入未启用", "请先在设置页面中启用远程输入功能", [ + { text: "取消", style: "cancel" }, + { text: "去设置", onPress: () => router.push("/settings") }, + ]); + return; + } + showRemoteModal(); + }; + const renderItem = ({ item }: { item: SearchResult }) => ( - + diff --git a/app/settings.tsx b/app/settings.tsx index 2922af9..f2e080b 100644 --- a/app/settings.tsx +++ b/app/settings.tsx @@ -6,13 +6,15 @@ import { ThemedView } from "@/components/ThemedView"; import { StyledButton } from "@/components/StyledButton"; import { useThemeColor } from "@/hooks/useThemeColor"; import { useSettingsStore } from "@/stores/settingsStore"; +import { useRemoteControlStore } from "@/stores/remoteControlStore"; import { APIConfigSection } from "@/components/settings/APIConfigSection"; import { LiveStreamSection } from "@/components/settings/LiveStreamSection"; import { RemoteInputSection } from "@/components/settings/RemoteInputSection"; import { VideoSourceSection } from "@/components/settings/VideoSourceSection"; export default function SettingsScreen() { - const { loadSettings, saveSettings } = useSettingsStore(); + const { loadSettings, saveSettings, setApiBaseUrl, setM3uUrl } = useSettingsStore(); + const { lastMessage } = useRemoteControlStore(); const backgroundColor = useThemeColor({}, "background"); const [hasChanges, setHasChanges] = useState(false); @@ -20,11 +22,32 @@ export default function SettingsScreen() { const [currentFocusIndex, setCurrentFocusIndex] = useState(0); const saveButtonRef = useRef(null); + const apiSectionRef = useRef(null); + const liveStreamSectionRef = useRef(null); useEffect(() => { loadSettings(); }, [loadSettings]); + useEffect(() => { + if (lastMessage) { + const realMessage = lastMessage.split("_")[0]; + handleRemoteInput(realMessage); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastMessage]); + + const handleRemoteInput = (message: string) => { + // Handle remote input based on currently focused section + if (currentFocusIndex === 1 && apiSectionRef.current) { + // API Config Section + setApiBaseUrl(message); + } else if (currentFocusIndex === 2 && liveStreamSectionRef.current) { + // Live Stream Section + setM3uUrl(message); + } + }; + const handleSave = async () => { setIsLoading(true); try { @@ -47,11 +70,19 @@ export default function SettingsScreen() { key: "remote", }, { - component: setCurrentFocusIndex(1)} />, + component: ( + setCurrentFocusIndex(1)} /> + ), key: "api", }, { - component: setCurrentFocusIndex(2)} />, + component: ( + setCurrentFocusIndex(2)} + /> + ), key: "livestream", }, { diff --git a/components/settings/APIConfigSection.tsx b/components/settings/APIConfigSection.tsx index 55a4c6a..e4b742a 100644 --- a/components/settings/APIConfigSection.tsx +++ b/components/settings/APIConfigSection.tsx @@ -1,9 +1,10 @@ -import React, { useState, useRef } from "react"; -import { View, TextInput, StyleSheet, Pressable, Animated } from "react-native"; +import React, { useState, useRef, useImperativeHandle, forwardRef } from "react"; +import { View, TextInput, StyleSheet, Animated } from "react-native"; import { useTVEventHandler } from "react-native"; import { ThemedText } from "@/components/ThemedText"; import { SettingsSection } from "./SettingsSection"; import { useSettingsStore } from "@/stores/settingsStore"; +import { useRemoteControlStore } from "@/stores/remoteControlStore"; import { useButtonAnimation } from "@/hooks/useAnimation"; interface APIConfigSectionProps { @@ -12,68 +13,99 @@ interface APIConfigSectionProps { onBlur?: () => void; } -export const APIConfigSection: React.FC = ({ onChanged, onFocus, onBlur }) => { - const { apiBaseUrl, setApiBaseUrl } = useSettingsStore(); - const [isInputFocused, setIsInputFocused] = useState(false); - const [isSectionFocused, setIsSectionFocused] = useState(false); - const inputRef = useRef(null); - const inputAnimationStyle = useButtonAnimation(isSectionFocused, 1.01); +export interface APIConfigSectionRef { + setInputValue: (value: string) => void; +} - const handleUrlChange = (url: string) => { - setApiBaseUrl(url); - onChanged(); - }; +export const APIConfigSection = forwardRef( + ({ onChanged, onFocus, onBlur }, ref) => { + const { apiBaseUrl, setApiBaseUrl, remoteInputEnabled } = useSettingsStore(); + const { serverUrl } = useRemoteControlStore(); + const [isInputFocused, setIsInputFocused] = useState(false); + const [isSectionFocused, setIsSectionFocused] = useState(false); + const inputRef = useRef(null); + const inputAnimationStyle = useButtonAnimation(isSectionFocused, 1.01); - const handleSectionFocus = () => { - setIsSectionFocused(true); - onFocus?.(); - }; + const handleUrlChange = (url: string) => { + setApiBaseUrl(url); + onChanged(); + }; - const handleSectionBlur = () => { - setIsSectionFocused(false); - onBlur?.(); - }; + useImperativeHandle(ref, () => ({ + setInputValue: (value: string) => { + setApiBaseUrl(value); + onChanged(); + }, + })); - // TV遥控器事件处理 - const handleTVEvent = React.useCallback( - (event: any) => { - if (isSectionFocused && event.eventType === "select") { - inputRef.current?.focus(); - } - }, - [isSectionFocused] - ); + const handleSectionFocus = () => { + setIsSectionFocused(true); + onFocus?.(); + }; - useTVEventHandler(handleTVEvent); + const handleSectionBlur = () => { + setIsSectionFocused(false); + onBlur?.(); + }; - return ( - - - API 地址 - - setIsInputFocused(true)} - onBlur={() => setIsInputFocused(false)} - /> - - - - ); -}; + // TV遥控器事件处理 + const handleTVEvent = React.useCallback( + (event: any) => { + if (isSectionFocused && event.eventType === "select") { + inputRef.current?.focus(); + } + }, + [isSectionFocused] + ); + + useTVEventHandler(handleTVEvent); + + return ( + + + + API 地址 + {remoteInputEnabled && serverUrl && ( + 用手机访问 {serverUrl},可远程输入 + )} + + + setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + /> + + + + ); + } +); + +APIConfigSection.displayName = "APIConfigSection"; const styles = StyleSheet.create({ + titleContainer: { + flexDirection: "row", + alignItems: "center", + marginBottom: 8, + }, sectionTitle: { fontSize: 16, fontWeight: "bold", - marginBottom: 8, + marginRight: 12, + }, + subtitle: { + fontSize: 12, + color: "#888", + fontStyle: "italic", }, inputContainer: { marginBottom: 12, diff --git a/components/settings/LiveStreamSection.tsx b/components/settings/LiveStreamSection.tsx index b271695..3a5517d 100644 --- a/components/settings/LiveStreamSection.tsx +++ b/components/settings/LiveStreamSection.tsx @@ -1,10 +1,11 @@ -import React, { useState, useRef } from 'react'; -import { View, TextInput, StyleSheet, Animated } from 'react-native'; -import { useTVEventHandler } from 'react-native'; -import { ThemedText } from '@/components/ThemedText'; -import { SettingsSection } from './SettingsSection'; -import { useSettingsStore } from '@/stores/settingsStore'; -import { useButtonAnimation } from '@/hooks/useAnimation'; +import React, { useState, useRef, useImperativeHandle, forwardRef } from "react"; +import { View, TextInput, StyleSheet, Animated } from "react-native"; +import { useTVEventHandler } from "react-native"; +import { ThemedText } from "@/components/ThemedText"; +import { SettingsSection } from "./SettingsSection"; +import { useSettingsStore } from "@/stores/settingsStore"; +import { useRemoteControlStore } from "@/stores/remoteControlStore"; +import { useButtonAnimation } from "@/hooks/useAnimation"; interface LiveStreamSectionProps { onChanged: () => void; @@ -12,67 +13,98 @@ interface LiveStreamSectionProps { onBlur?: () => void; } -export const LiveStreamSection: React.FC = ({ onChanged, onFocus, onBlur }) => { - const { m3uUrl, setM3uUrl } = useSettingsStore(); - const [isInputFocused, setIsInputFocused] = useState(false); - const [isSectionFocused, setIsSectionFocused] = useState(false); - const inputRef = useRef(null); - const inputAnimationStyle = useButtonAnimation(isSectionFocused, 1.01); +export interface LiveStreamSectionRef { + setInputValue: (value: string) => void; +} - const handleUrlChange = (url: string) => { - setM3uUrl(url); - onChanged(); - }; +export const LiveStreamSection = forwardRef( + ({ onChanged, onFocus, onBlur }, ref) => { + const { m3uUrl, setM3uUrl, remoteInputEnabled } = useSettingsStore(); + const { serverUrl } = useRemoteControlStore(); + const [isInputFocused, setIsInputFocused] = useState(false); + const [isSectionFocused, setIsSectionFocused] = useState(false); + const inputRef = useRef(null); + const inputAnimationStyle = useButtonAnimation(isSectionFocused, 1.01); - const handleSectionFocus = () => { - setIsSectionFocused(true); - onFocus?.(); - }; + const handleUrlChange = (url: string) => { + setM3uUrl(url); + onChanged(); + }; - const handleSectionBlur = () => { - setIsSectionFocused(false); - onBlur?.(); - }; + useImperativeHandle(ref, () => ({ + setInputValue: (value: string) => { + setM3uUrl(value); + onChanged(); + }, + })); - const handleTVEvent = React.useCallback( - (event: any) => { - if (isSectionFocused && event.eventType === "select") { - inputRef.current?.focus(); - } - }, - [isSectionFocused] - ); + const handleSectionFocus = () => { + setIsSectionFocused(true); + onFocus?.(); + }; - useTVEventHandler(handleTVEvent); + const handleSectionBlur = () => { + setIsSectionFocused(false); + onBlur?.(); + }; - return ( - - - 直播源地址 - - setIsInputFocused(true)} - onBlur={() => setIsInputFocused(false)} - /> - - - - ); -}; + const handleTVEvent = React.useCallback( + (event: any) => { + if (isSectionFocused && event.eventType === "select") { + inputRef.current?.focus(); + } + }, + [isSectionFocused] + ); + + useTVEventHandler(handleTVEvent); + + return ( + + + + 直播源地址 + {remoteInputEnabled && serverUrl && ( + 用手机访问 {serverUrl},可远程输入 + )} + + + setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + /> + + + + ); + } +); + +LiveStreamSection.displayName = "LiveStreamSection"; const styles = StyleSheet.create({ + titleContainer: { + flexDirection: "row", + alignItems: "center", + marginBottom: 8, + }, sectionTitle: { fontSize: 16, - fontWeight: 'bold', - marginBottom: 8, + fontWeight: "bold", + marginRight: 12, + }, + subtitle: { + fontSize: 12, + color: "#888", + fontStyle: "italic", }, inputContainer: { marginBottom: 12, @@ -83,16 +115,16 @@ const styles = StyleSheet.create({ borderRadius: 8, paddingHorizontal: 15, fontSize: 16, - backgroundColor: '#3a3a3c', - color: 'white', - borderColor: 'transparent', + backgroundColor: "#3a3a3c", + color: "white", + borderColor: "transparent", }, inputFocused: { - borderColor: '#007AFF', - shadowColor: '#007AFF', + borderColor: "#007AFF", + shadowColor: "#007AFF", shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.8, shadowRadius: 10, elevation: 5, }, -}); \ No newline at end of file +}); diff --git a/services/storage.ts b/services/storage.ts index c8895b5..e58dca0 100644 --- a/services/storage.ts +++ b/services/storage.ts @@ -184,7 +184,7 @@ export class SettingsManager { static async get(): Promise { const defaultSettings: AppSettings = { apiBaseUrl: "https://orion-tv.edu.deal", - remoteInputEnabled: false, + remoteInputEnabled: true, videoSource: { enabledAll: true, sources: {}, diff --git a/stores/remoteControlStore.ts b/stores/remoteControlStore.ts index b47aace..4d816a9 100644 --- a/stores/remoteControlStore.ts +++ b/stores/remoteControlStore.ts @@ -25,6 +25,16 @@ export const useRemoteControlStore = create((set, get) => ({ if (get().isServerRunning) { return; } + remoteControlService.init({ + onMessage: (message: string) => { + console.log('[RemoteControlStore] Received message:', message); + set({ lastMessage: message }); + }, + onHandshake: () => { + console.log('[RemoteControlStore] Handshake successful'); + set({ isModalVisible: false }) + }, + }); try { const url = await remoteControlService.startServer(); console.log(`[RemoteControlStore] Server started, URL: ${url}`);