import React, { useState, useRef, useEffect, useCallback } from "react"; import { View, TextInput, StyleSheet, FlatList, ActivityIndicator, Text, Keyboard, PermissionsAndroid, Platform, } from "react-native"; import { ThemedView } from "@/components/ThemedView"; import { ThemedText } from "@/components/ThemedText"; import VideoCard from "@/components/VideoCard.tv"; import { api, SearchResult } from "@/services/api"; import { Search, Mic } from "lucide-react-native"; import { StyledButton } from "@/components/StyledButton"; import Voice from "@react-native-voice/voice"; export default function SearchScreen() { const [keyword, setKeyword] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const textInputRef = useRef(null); const colorScheme = "dark"; // Replace with useColorScheme() if needed const [isInputFocused, setIsInputFocused] = useState(false); const [isListening, setIsListening] = useState(false); const onSpeechResults = useCallback((e: any) => { if (e.value && e.value.length > 0) { setKeyword(e.value[0]); } }, []); const onSpeechEnd = useCallback(() => { setIsListening(false); }, []); const onSpeechError = useCallback((e: any) => { console.error(e); setIsListening(false); }, []); useEffect(() => { Voice.onSpeechResults = onSpeechResults; Voice.onSpeechEnd = onSpeechEnd; Voice.onSpeechError = onSpeechError; return () => { Voice.destroy().then(() => Voice.removeAllListeners()); }; }, [onSpeechResults, onSpeechEnd, onSpeechError]); useEffect(() => { // Focus the text input when the screen loads const timer = setTimeout(() => { textInputRef.current?.focus(); }, 200); return () => clearTimeout(timer); }, []); const startListening = async () => { if (Platform.OS === "android") { const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO); if (!hasPermission) { try { const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, { title: "Microphone Permission", message: "App needs access to your microphone to enable voice search.", buttonNeutral: "Ask Me Later", buttonNegative: "Cancel", buttonPositive: "OK", }); if (granted !== PermissionsAndroid.RESULTS.GRANTED) { console.log("Microphone permission denied"); return; } } catch (err) { console.warn(err); return; } } } try { await Voice.start("zh-CN"); setIsListening(true); } catch (e) { console.error(e); } }; const stopListening = async () => { try { await Voice.stop(); setIsListening(false); } catch (e) { console.error(e); } }; const handleVoiceSearch = () => { if (isListening) { stopListening(); } else { startListening(); } }; const handleSearch = async () => { if (!keyword.trim()) { Keyboard.dismiss(); return; } Keyboard.dismiss(); setLoading(true); setError(null); try { const response = await api.searchVideos(keyword); if (response.results.length > 0) { setResults(response.results); } else { setError("没有找到相关内容"); } } catch (err) { setError("搜索失败,请稍后重试。"); console.error("Search failed:", err); } finally { setLoading(false); } }; const renderItem = ({ item }: { item: SearchResult }) => ( ); return ( setIsInputFocused(true)} onBlur={() => setIsInputFocused(false)} onSubmitEditing={handleSearch} // Allow searching with remote 'enter' button returnKeyType="search" /> {loading ? ( ) : error ? ( {error} ) : ( `${item.id}-${item.source}-${index}`} numColumns={5} // Adjust based on your card size and desired layout contentContainerStyle={styles.listContent} ListEmptyComponent={ 输入关键词开始搜索 } /> )} ); } const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 50, }, searchContainer: { flexDirection: "row", paddingHorizontal: 20, marginBottom: 20, alignItems: "center", }, input: { flex: 1, height: 50, backgroundColor: "#2c2c2e", // Default for dark mode, overridden inline borderRadius: 8, paddingHorizontal: 15, color: "white", // Default for dark mode, overridden inline fontSize: 18, marginRight: 10, borderWidth: 2, borderColor: "transparent", // Default, overridden for focus }, searchButton: { padding: 12, // backgroundColor is now set dynamically borderRadius: 8, }, centerContainer: { flex: 1, justifyContent: "center", alignItems: "center", }, errorText: { color: "red", }, listContent: { paddingHorizontal: 10, }, });