feat(ui): enhance settings sections with responsive layout and keyboard handling

This commit is contained in:
James Chen
2025-09-01 14:06:09 +08:00
parent 62d8141178
commit 0d9f552ede
7 changed files with 114 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { View, StyleSheet, FlatList, Alert, KeyboardAvoidingView, Platform } from "react-native";
import { View, StyleSheet, FlatList, Alert, KeyboardAvoidingView, Platform, ScrollView } from "react-native";
import { useTVEventHandler } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ThemedText } from "@/components/ThemedText";
@@ -20,6 +20,7 @@ import { getCommonResponsiveStyles } from "@/utils/ResponsiveStyles";
import ResponsiveNavigation from "@/components/navigation/ResponsiveNavigation";
import ResponsiveHeader from "@/components/navigation/ResponsiveHeader";
import { DeviceUtils } from "@/utils/DeviceUtils";
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
export default function SettingsScreen() {
const { loadSettings, saveSettings, setApiBaseUrl, setM3uUrl } = useSettingsStore();
@@ -164,13 +165,22 @@ export default function SettingsScreen() {
[currentFocusIndex, sections.length, deviceType]
);
useTVEventHandler(deviceType === "tv" ? handleTVEvent : () => {});
useTVEventHandler(deviceType === "tv" ? handleTVEvent : () => { });
// 动态样式
const dynamicStyles = createResponsiveStyles(deviceType, spacing, insets);
const renderSettingsContent = () => (
<KeyboardAvoidingView style={{ flex: 1, backgroundColor }} behavior={Platform.OS === "ios" ? "padding" : "height"}>
// <KeyboardAvoidingView style={{ flex: 1, backgroundColor }} behavior={Platform.OS === "ios" ? "padding" : "height"}>
<KeyboardAwareScrollView
enableOnAndroid={true}
extraScrollHeight={20}
keyboardOpeningTime={0}
keyboardShouldPersistTaps="always"
scrollEnabled={true}
style={{ flex: 1, backgroundColor }}
>
<ThemedView style={[commonStyles.container, dynamicStyles.container]}>
{deviceType === "tv" && (
<View style={dynamicStyles.header}>
@@ -203,7 +213,8 @@ export default function SettingsScreen() {
/>
</View>
</ThemedView>
</KeyboardAvoidingView>
</KeyboardAwareScrollView>
// </KeyboardAvoidingView>
);
// 根据设备类型决定是否包装在响应式导航中

View File

@@ -1,5 +1,5 @@
import React, { useState, useRef, useImperativeHandle, forwardRef } from "react";
import { View, TextInput, StyleSheet, Animated } from "react-native";
import { View, TextInput, StyleSheet, Animated, Platform } from "react-native";
import { useTVEventHandler } from "react-native";
import { ThemedText } from "@/components/ThemedText";
import { SettingsSection } from "./SettingsSection";
@@ -7,11 +7,13 @@ 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;
}
@@ -20,13 +22,14 @@ export interface APIConfigSectionRef {
}
export const APIConfigSection = forwardRef<APIConfigSectionRef, APIConfigSectionProps>(
({ onChanged, onFocus, onBlur, hideDescription = false }, ref) => {
({ 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);
@@ -60,10 +63,17 @@ export const APIConfigSection = forwardRef<APIConfigSectionRef, APIConfigSection
[isSectionFocused]
);
const handlePress = () => {
inputRef.current?.focus();
onPress?.();
}
useTVEventHandler(handleTVEvent);
return (
<SettingsSection focusable onFocus={handleSectionFocus} onBlur={handleSectionBlur}>
<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>

View File

@@ -1,5 +1,5 @@
import React, { useState, useRef, useImperativeHandle, forwardRef } from "react";
import { View, TextInput, StyleSheet, Animated } from "react-native";
import { View, TextInput, StyleSheet, Animated, Platform } from "react-native";
import { useTVEventHandler } from "react-native";
import { ThemedText } from "@/components/ThemedText";
import { SettingsSection } from "./SettingsSection";
@@ -7,11 +7,13 @@ 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 LiveStreamSectionProps {
onChanged: () => void;
onFocus?: () => void;
onBlur?: () => void;
onPress?: () => void;
}
export interface LiveStreamSectionRef {
@@ -19,13 +21,14 @@ export interface LiveStreamSectionRef {
}
export const LiveStreamSection = forwardRef<LiveStreamSectionRef, LiveStreamSectionProps>(
({ onChanged, onFocus, onBlur }, ref) => {
({ onChanged, onFocus, onBlur, onPress }, ref) => {
const { m3uUrl, setM3uUrl, 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) => {
setM3uUrl(url);
@@ -49,6 +52,11 @@ export const LiveStreamSection = forwardRef<LiveStreamSectionRef, LiveStreamSect
onBlur?.();
};
const handlePress = () => {
inputRef.current?.focus();
onPress?.();
}
const handleTVEvent = React.useCallback(
(event: any) => {
if (isSectionFocused && event.eventType === "select") {
@@ -61,7 +69,9 @@ export const LiveStreamSection = forwardRef<LiveStreamSectionRef, LiveStreamSect
useTVEventHandler(handleTVEvent);
return (
<SettingsSection focusable onFocus={handleSectionFocus} onBlur={handleSectionBlur}>
<SettingsSection focusable onFocus={handleSectionFocus} onBlur={handleSectionBlur}
onPress={Platform.isTV||deviceType !=='tv' ? undefined : handlePress}
>
<View style={styles.inputContainer}>
<View style={styles.titleContainer}>
<ThemedText style={styles.sectionTitle}></ThemedText>
@@ -81,6 +91,7 @@ export const LiveStreamSection = forwardRef<LiveStreamSectionRef, LiveStreamSect
autoCorrect={false}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
// onPress={handlePress}
/>
</Animated.View>
</View>

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from "react";
import { View, Switch, StyleSheet, Pressable, Animated } from "react-native";
import { View, Switch, StyleSheet, Pressable, Animated, Platform } from "react-native";
import { useTVEventHandler } from "react-native";
import { ThemedText } from "@/components/ThemedText";
import { SettingsSection } from "./SettingsSection";
@@ -7,18 +7,21 @@ 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 RemoteInputSectionProps {
onChanged: () => void;
onFocus?: () => void;
onBlur?: () => void;
onPress?: () => void;
}
export const RemoteInputSection: React.FC<RemoteInputSectionProps> = ({ onChanged, onFocus, onBlur }) => {
export const RemoteInputSection: React.FC<RemoteInputSectionProps> = ({ onChanged, onFocus, onBlur, onPress }) => {
const { remoteInputEnabled, setRemoteInputEnabled } = useSettingsStore();
const { isServerRunning, serverUrl, error } = useRemoteControlStore();
const [isFocused, setIsFocused] = React.useState(false);
const animationStyle = useButtonAnimation(isFocused, 1.2);
const deviceType = useResponsiveLayout().deviceType;
const handleToggle = useCallback(
(enabled: boolean) => {
@@ -38,6 +41,10 @@ export const RemoteInputSection: React.FC<RemoteInputSectionProps> = ({ onChange
onBlur?.();
};
const handlePress = () => {
handleToggle(!remoteInputEnabled);
}
// TV遥控器事件处理
const handleTVEvent = React.useCallback(
(event: any) => {
@@ -51,7 +58,9 @@ export const RemoteInputSection: React.FC<RemoteInputSectionProps> = ({ onChange
useTVEventHandler(handleTVEvent);
return (
<SettingsSection focusable onFocus={handleSectionFocus} onBlur={handleSectionBlur}>
<SettingsSection focusable onFocus={handleSectionFocus} onBlur={handleSectionBlur}
{...Platform.isTV||deviceType !=='tv'? undefined :{onPress:handlePress}}
>
<Pressable style={styles.settingItem} onFocus={handleSectionFocus} onBlur={handleSectionBlur}>
<View style={styles.settingInfo}>
<ThemedText style={styles.settingName}></ThemedText>

View File

@@ -1,17 +1,20 @@
import React, { useState } from "react";
import { StyleSheet, Pressable } from "react-native";
import { StyleSheet, Pressable, Platform } from "react-native";
import { ThemedView } from "@/components/ThemedView";
import { Colors } from "@/constants/Colors";
import { useResponsiveLayout } from "@/hooks/useResponsiveLayout";
interface SettingsSectionProps {
children: React.ReactNode;
onFocus?: () => void;
onBlur?: () => void;
onPress?: () => void;
focusable?: boolean;
}
export const SettingsSection: React.FC<SettingsSectionProps> = ({ children, onFocus, onBlur, focusable = false }) => {
export const SettingsSection: React.FC<SettingsSectionProps> = ({ children, onFocus, onBlur, onPress, focusable = false }) => {
const [isFocused, setIsFocused] = useState(false);
const deviceType = useResponsiveLayout().deviceType;
const handleFocus = () => {
setIsFocused(true);
@@ -23,13 +26,24 @@ export const SettingsSection: React.FC<SettingsSectionProps> = ({ children, onFo
onBlur?.();
};
const handlePress = () => {
onPress?.();
}
if (!focusable) {
return <ThemedView style={styles.section}>{children}</ThemedView>;
}
return (
<ThemedView style={[styles.section, isFocused && styles.sectionFocused]}>
<Pressable style={styles.sectionPressable} onFocus={handleFocus} onBlur={handleBlur}>
<Pressable
android_ripple={Platform.isTV||deviceType !=='tv'? {color:'transparent'}:{color:Colors.dark.link}}
style={styles.sectionPressable}
// {...(Platform.isTV ? {onFocus: handleFocus, onBlur: handleBlur} : {onPress: onPress})}
onFocus={handleFocus}
onBlur={handleBlur}
onPress={handlePress}
>
{children}
</Pressable>
</ThemedView>

View File

@@ -50,6 +50,7 @@
"react-native-blob-util": "^0.22.2",
"react-native-file-viewer": "^2.1.5",
"react-native-gesture-handler": "~2.16.1",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-media-console": "*",
"react-native-qrcode-svg": "^6.3.1",
"react-native-reanimated": "~3.10.1",

View File

@@ -7775,7 +7775,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2, prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.7.2, prop-types@^15.8.0, prop-types@^15.8.1:
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.0, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -7944,6 +7944,19 @@ react-native-helmet-async@2.0.4:
react-fast-compare "^3.2.2"
shallowequal "^1.1.0"
react-native-iphone-x-helper@^1.0.3:
version "1.3.1"
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"
integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==
react-native-keyboard-aware-scroll-view@^0.9.5:
version "0.9.5"
resolved "https://registry.yarnpkg.com/react-native-keyboard-aware-scroll-view/-/react-native-keyboard-aware-scroll-view-0.9.5.tgz#e2e9665d320c188e6b1f22f151b94eb358bf9b71"
integrity sha512-XwfRn+T/qBH9WjTWIBiJD2hPWg0yJvtaEw6RtPCa5/PYHabzBaWxYBOl0usXN/368BL1XktnZPh8C2lmTpOREA==
dependencies:
prop-types "^15.6.2"
react-native-iphone-x-helper "^1.0.3"
react-native-media-console@*:
version "2.2.4"
resolved "https://registry.yarnpkg.com/react-native-media-console/-/react-native-media-console-2.2.4.tgz#76a232cdcb645cfdb25bacddee514f360eb4947d"
@@ -8832,7 +8845,16 @@ string-length@^5.0.1:
char-regex "^2.0.0"
strip-ansi "^7.0.1"
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -8923,7 +8945,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -8937,6 +8959,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -9768,7 +9797,7 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -9786,6 +9815,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"