From 0d9f552ede06328dabdea9d5dd5f3cd903d4adbf Mon Sep 17 00:00:00 2001 From: James Chen Date: Mon, 1 Sep 2025 14:06:09 +0800 Subject: [PATCH] feat(ui): enhance settings sections with responsive layout and keyboard handling --- app/settings.tsx | 19 +++++++-- components/settings/APIConfigSection.tsx | 16 ++++++-- components/settings/LiveStreamSection.tsx | 17 ++++++-- components/settings/RemoteInputSection.tsx | 15 +++++-- components/settings/SettingsSection.tsx | 20 ++++++++-- package.json | 1 + yarn.lock | 46 ++++++++++++++++++++-- 7 files changed, 114 insertions(+), 20 deletions(-) diff --git a/app/settings.tsx b/app/settings.tsx index 8a0eace..d511000 100644 --- a/app/settings.tsx +++ b/app/settings.tsx @@ -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 = () => ( - + // + + {deviceType === "tv" && ( @@ -203,7 +213,8 @@ export default function SettingsScreen() { /> - + + // ); // 根据设备类型决定是否包装在响应式导航中 diff --git a/components/settings/APIConfigSection.tsx b/components/settings/APIConfigSection.tsx index 7868eac..218b806 100644 --- a/components/settings/APIConfigSection.tsx +++ b/components/settings/APIConfigSection.tsx @@ -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( - ({ 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(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 { + inputRef.current?.focus(); + onPress?.(); + } + useTVEventHandler(handleTVEvent); return ( - + API 地址 diff --git a/components/settings/LiveStreamSection.tsx b/components/settings/LiveStreamSection.tsx index d09e680..bd1a56d 100644 --- a/components/settings/LiveStreamSection.tsx +++ b/components/settings/LiveStreamSection.tsx @@ -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( - ({ 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(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 { + inputRef.current?.focus(); + onPress?.(); + } + const handleTVEvent = React.useCallback( (event: any) => { if (isSectionFocused && event.eventType === "select") { @@ -61,7 +69,9 @@ export const LiveStreamSection = forwardRef + 直播源地址 @@ -81,6 +91,7 @@ export const LiveStreamSection = forwardRef setIsInputFocused(true)} onBlur={() => setIsInputFocused(false)} + // onPress={handlePress} /> diff --git a/components/settings/RemoteInputSection.tsx b/components/settings/RemoteInputSection.tsx index 7a7323a..77218d0 100644 --- a/components/settings/RemoteInputSection.tsx +++ b/components/settings/RemoteInputSection.tsx @@ -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 = ({ onChanged, onFocus, onBlur }) => { +export const RemoteInputSection: React.FC = ({ 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 = ({ onChange onBlur?.(); }; + const handlePress = () => { + handleToggle(!remoteInputEnabled); + } + // TV遥控器事件处理 const handleTVEvent = React.useCallback( (event: any) => { @@ -51,7 +58,9 @@ export const RemoteInputSection: React.FC = ({ onChange useTVEventHandler(handleTVEvent); return ( - + 启用远程输入 diff --git a/components/settings/SettingsSection.tsx b/components/settings/SettingsSection.tsx index 2334581..d857f3f 100644 --- a/components/settings/SettingsSection.tsx +++ b/components/settings/SettingsSection.tsx @@ -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 = ({ children, onFocus, onBlur, focusable = false }) => { +export const SettingsSection: React.FC = ({ 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 = ({ children, onFo onBlur?.(); }; + const handlePress = () => { + onPress?.(); + } + if (!focusable) { return {children}; } return ( - + {children} diff --git a/package.json b/package.json index d492b18..ad804e4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 6573989..d4832c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"