Files
OrionTV/components/settings/APIConfigSection.tsx

174 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useRef, useImperativeHandle, forwardRef } from "react";
import { View, TextInput, StyleSheet, Animated, Platform } 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";
import { Colors } from "@/constants/Colors";
import { useResponsiveLayout } from "@/hooks/useResponsiveLayout";
interface APIConfigSectionProps {
onChanged: () => void;
onFocus?: () => void;
onBlur?: () => void;
onPress?: () => void;
hideDescription?: boolean;
}
export interface APIConfigSectionRef {
setInputValue: (value: string) => void;
}
export const APIConfigSection = forwardRef<APIConfigSectionRef, APIConfigSectionProps>(
({ onChanged, onFocus, onBlur, onPress, hideDescription = false }, ref) => {
const { apiBaseUrl, setApiBaseUrl, remoteInputEnabled } = useSettingsStore();
const { serverUrl } = useRemoteControlStore();
const [isInputFocused, setIsInputFocused] = useState(false);
const [isSectionFocused, setIsSectionFocused] = useState(false);
const inputRef = useRef<TextInput>(null);
const inputAnimationStyle = useButtonAnimation(isSectionFocused, 1.01);
const deviceType = useResponsiveLayout().deviceType;
const handleUrlChange = (url: string) => {
setApiBaseUrl(url);
onChanged();
};
useImperativeHandle(ref, () => ({
setInputValue: (value: string) => {
setApiBaseUrl(value);
onChanged();
},
}));
const handleSectionFocus = () => {
setIsSectionFocused(true);
onFocus?.();
};
const handleSectionBlur = () => {
setIsSectionFocused(false);
onBlur?.();
};
// TV遥控器事件处理
const handleTVEvent = React.useCallback(
(event: any) => {
if (isSectionFocused && event.eventType === "select") {
inputRef.current?.focus();
}
},
[isSectionFocused]
);
const handlePress = () => {
inputRef.current?.focus();
onPress?.();
}
useTVEventHandler(handleTVEvent);
const [selection, setSelection] = useState<{ start: number; end: number }>({
start: 0,
end: 0,
});
// 当用户手动移动光标或选中文本时,同步到 state可选
const onSelectionChange = ({
nativeEvent: { selection },
}: any) => {
setSelection(selection);
};
return (
<SettingsSection focusable onFocus={handleSectionFocus} onBlur={handleSectionBlur}
{...Platform.isTV || deviceType !== 'tv' ? undefined : { onPress: handlePress }}
>
<View style={styles.inputContainer}>
<View style={styles.titleContainer}>
<ThemedText style={styles.sectionTitle}>API </ThemedText>
{!hideDescription && remoteInputEnabled && serverUrl && (
<ThemedText style={styles.subtitle}>访 {serverUrl}</ThemedText>
)}
</View>
<Animated.View style={inputAnimationStyle}>
<TextInput
ref={inputRef}
style={[styles.input, isInputFocused && styles.inputFocused]}
value={apiBaseUrl}
onChangeText={handleUrlChange}
placeholder="输入服务器地址"
placeholderTextColor="#888"
autoCapitalize="none"
autoCorrect={false}
onFocus={() => {
setIsInputFocused(true);
// 将光标移动到文本末尾
const end = apiBaseUrl.length;
setSelection({ start: end, end: end });
// 有时需要延迟一下,让系统先完成 focus 再设置 selection
//(在 Android 上更可靠)
setTimeout(() => {
// 对于受控的 selection 已经生效,这里仅作保险
inputRef.current?.setNativeProps({ selection: { start: end, end: end } });
}, 0);
}}
selection={selection}
onSelectionChange={onSelectionChange} // 可选
onBlur={() => setIsInputFocused(false)}
/>
</Animated.View>
</View>
</SettingsSection>
);
}
);
APIConfigSection.displayName = "APIConfigSection";
const styles = StyleSheet.create({
titleContainer: {
flexDirection: "row",
alignItems: "center",
marginBottom: 8,
},
sectionTitle: {
fontSize: 16,
fontWeight: "bold",
marginRight: 12,
},
subtitle: {
fontSize: 12,
color: "#888",
fontStyle: "italic",
},
inputContainer: {
marginBottom: 12,
},
label: {
fontSize: 16,
marginBottom: 8,
color: "#ccc",
},
input: {
height: 50,
borderWidth: 2,
borderRadius: 8,
paddingHorizontal: 15,
fontSize: 16,
backgroundColor: "#3a3a3c",
color: "white",
borderColor: "transparent",
},
inputFocused: {
borderColor: Colors.dark.primary,
shadowColor: Colors.dark.primary,
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.8,
shadowRadius: 10,
elevation: 5,
},
});