diff --git a/app/search.tsx b/app/search.tsx index 003fa03..fb4e986 100644 --- a/app/search.tsx +++ b/app/search.tsx @@ -1,11 +1,22 @@ import React, { useState, useRef, useEffect } from "react"; -import { View, TextInput, StyleSheet, FlatList, ActivityIndicator, Text, Keyboard } from "react-native"; +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 } from "lucide-react-native"; +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(""); @@ -15,6 +26,32 @@ export default function SearchScreen() { const textInputRef = useRef(null); const colorScheme = "dark"; // Replace with useColorScheme() if needed const [isInputFocused, setIsInputFocused] = useState(false); + const [isListening, setIsListening] = useState(false); + + const onSpeechResults = (e: any) => { + if (e.value && e.value.length > 0) { + setKeyword(e.value[0]); + } + }; + + const onSpeechEnd = () => { + setIsListening(false); + }; + + const onSpeechError = (e: any) => { + console.error(e); + setIsListening(false); + }; + + useEffect(() => { + Voice.onSpeechResults = onSpeechResults; + Voice.onSpeechEnd = onSpeechEnd; + Voice.onSpeechError = onSpeechError; + + return () => { + Voice.destroy().then(Voice.removeAllListeners); + }; + }, []); useEffect(() => { // Focus the text input when the screen loads @@ -24,6 +61,54 @@ export default function SearchScreen() { 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(); @@ -81,6 +166,9 @@ export default function SearchScreen() { onSubmitEditing={handleSearch} // Allow searching with remote 'enter' button returnKeyType="search" /> + + + diff --git a/package.json b/package.json index 6749f8b..217c584 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "dependencies": { "@expo/vector-icons": "^14.0.0", "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-voice/voice": "^3.2.4", "@react-navigation/native": "^6.0.2", "expo": "~51.0.13", "expo-av": "~14.0.7", diff --git a/yarn.lock b/yarn.lock index 2d23161..35e9bda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -946,6 +946,29 @@ xcode "^3.0.1" xml2js "0.6.0" +"@expo/config-plugins@^2.0.0": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-2.0.4.tgz#955fd70a2aeefbe99ec71cecb1d7ea7b626dc79e" + integrity sha512-JGt/X2tFr7H8KBQrKfbGo9hmCubQraMxq5sj3bqDdKmDOLcE1a/EDCP9g0U4GHsa425J8VDIkQUHYz3h3ndEXQ== + dependencies: + "@expo/config-types" "^41.0.0" + "@expo/json-file" "8.2.30" + "@expo/plist" "0.0.13" + debug "^4.3.1" + find-up "~5.0.0" + fs-extra "9.0.0" + getenv "^1.0.0" + glob "7.1.6" + resolve-from "^5.0.0" + slash "^3.0.0" + xcode "^3.0.1" + xml2js "^0.4.23" + +"@expo/config-types@^41.0.0": + version "41.0.0" + resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-41.0.0.tgz#ffe1444c6c26e0e3a8f7149b4afe486e357536d1" + integrity sha512-Ax0pHuY5OQaSrzplOkT9DdpdmNzaVDnq9VySb4Ujq7UJ4U4jriLy8u93W98zunOXpcu0iiKubPsqD6lCiq0pig== + "@expo/config-types@^51.0.3": version "51.0.3" resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-51.0.3.tgz#520bdce5fd75f9d234fd81bd0347443086419450" @@ -1004,6 +1027,16 @@ semver "^7.6.0" tempy "0.3.0" +"@expo/json-file@8.2.30": + version "8.2.30" + resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.30.tgz#bd855b6416b5c3af7e55b43f6761c1e7d2b755b0" + integrity sha512-vrgGyPEXBoFI5NY70IegusCSoSVIFV3T3ry4tjJg1MFQKTUlR7E0r+8g8XR6qC705rc2PawaZQjqXMAVtV6s2A== + dependencies: + "@babel/code-frame" "~7.10.4" + fs-extra "9.0.0" + json5 "^1.0.1" + write-file-atomic "^2.3.0" + "@expo/json-file@^8.3.0", "@expo/json-file@~8.3.0": version "8.3.3" resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.3.3.tgz#7926e3592f76030ce63d6b1308ac8f5d4d9341f4" @@ -1070,6 +1103,15 @@ ora "^3.4.0" resolve-workspace-root "^2.0.0" +"@expo/plist@0.0.13": + version "0.0.13" + resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.13.tgz#700a48d9927aa2b0257c613e13454164e7371a96" + integrity sha512-zGPSq9OrCn7lWvwLLHLpHUUq2E40KptUFXn53xyZXPViI0k9lbApcR9KlonQZ95C+ELsf0BQ3gRficwK92Ivcw== + dependencies: + base64-js "^1.2.3" + xmlbuilder "^14.0.0" + xmldom "~0.5.0" + "@expo/plist@^0.1.0": version "0.1.3" resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.1.3.tgz#b4fbee2c4f7a88512a4853d85319f4d95713c529" @@ -1685,6 +1727,14 @@ invariant "^2.2.4" nullthrows "^1.1.1" +"@react-native-voice/voice@^3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@react-native-voice/voice/-/voice-3.2.4.tgz#a0f9e5986c3c290155dd6e35ed192dab1c453f2c" + integrity sha512-4i3IpB/W5VxCI7BQZO5Nr2VB0ecx0SLvkln2Gy29cAQKqgBl+1ZsCwUBChwHlPbmja6vA3tp/+2ADQGwB1OhHg== + dependencies: + "@expo/config-plugins" "^2.0.0" + invariant "^2.2.4" + "@react-native/assets-registry@0.74.84": version "0.74.84" resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.74.84.tgz#aa472f82c1b7d8a30098c8ba22fad7b3dbb5be5f" @@ -5713,6 +5763,13 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== +json5@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" @@ -8826,6 +8883,14 @@ xml2js@0.6.0: sax ">=0.6.0" xmlbuilder "~11.0.0" +xml2js@^0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + xmlbuilder@^14.0.0: version "14.0.0" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-14.0.0.tgz#876b5aec4f05ffd5feb97b0a871c855d16fbeb8c" @@ -8846,6 +8911,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmldom@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e" + integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA== + xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"