Files
OrionTV/app/settings.tsx
zimplexing 13fade2113 fix(remote-input): resolve cross-page interference between settings and search pages
- Add targetPage field to remoteControlStore for message routing
- Update settings page to filter messages by target page
- Update search page to filter messages and pass target context
- Add clearMessage method to prevent duplicate message handling
- Ensure remote input only affects intended target page
2025-08-13 17:28:02 +08:00

261 lines
8.0 KiB
TypeScript

import React, { useState, useEffect, useRef } from "react";
import { View, StyleSheet, FlatList, Alert, KeyboardAvoidingView, Platform } from "react-native";
import { useTVEventHandler } from "react-native";
import { ThemedText } from "@/components/ThemedText";
import { ThemedView } from "@/components/ThemedView";
import { StyledButton } from "@/components/StyledButton";
import { useThemeColor } from "@/hooks/useThemeColor";
import { useSettingsStore } from "@/stores/settingsStore";
// import useAuthStore from "@/stores/authStore";
import { useRemoteControlStore } from "@/stores/remoteControlStore";
import { APIConfigSection } from "@/components/settings/APIConfigSection";
import { LiveStreamSection } from "@/components/settings/LiveStreamSection";
import { RemoteInputSection } from "@/components/settings/RemoteInputSection";
import { UpdateSection } from "@/components/settings/UpdateSection";
// import { VideoSourceSection } from "@/components/settings/VideoSourceSection";
import Toast from "react-native-toast-message";
import { useResponsiveLayout } from "@/hooks/useResponsiveLayout";
import { getCommonResponsiveStyles } from "@/utils/ResponsiveStyles";
import ResponsiveNavigation from "@/components/navigation/ResponsiveNavigation";
import ResponsiveHeader from "@/components/navigation/ResponsiveHeader";
import { DeviceUtils } from "@/utils/DeviceUtils";
export default function SettingsScreen() {
const { loadSettings, saveSettings, setApiBaseUrl, setM3uUrl } = useSettingsStore();
const { lastMessage, targetPage, clearMessage } = useRemoteControlStore();
const backgroundColor = useThemeColor({}, "background");
// 响应式布局配置
const responsiveConfig = useResponsiveLayout();
const commonStyles = getCommonResponsiveStyles(responsiveConfig);
const { deviceType, spacing } = responsiveConfig;
const [hasChanges, setHasChanges] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [currentFocusIndex, setCurrentFocusIndex] = useState(0);
const [currentSection, setCurrentSection] = useState<string | null>(null);
const saveButtonRef = useRef<any>(null);
const apiSectionRef = useRef<any>(null);
const liveStreamSectionRef = useRef<any>(null);
useEffect(() => {
loadSettings();
}, [loadSettings]);
useEffect(() => {
if (lastMessage && !targetPage) {
const realMessage = lastMessage.split("_")[0];
handleRemoteInput(realMessage);
clearMessage(); // Clear the message after processing
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastMessage, targetPage]);
const handleRemoteInput = (message: string) => {
// Handle remote input based on currently focused section
if (currentSection === "api" && apiSectionRef.current) {
// API Config Section
setApiBaseUrl(message);
} else if (currentSection === "livestream" && liveStreamSectionRef.current) {
// Live Stream Section
setM3uUrl(message);
}
};
const handleSave = async () => {
setIsLoading(true);
try {
await saveSettings();
setHasChanges(false);
Toast.show({
type: "success",
text1: "保存成功",
});
} catch {
Alert.alert("错误", "保存设置失败");
} finally {
setIsLoading(false);
}
};
const markAsChanged = () => {
setHasChanges(true);
};
const sections = [
{
component: (
<RemoteInputSection
onChanged={markAsChanged}
onFocus={() => {
setCurrentFocusIndex(0);
setCurrentSection("remote");
}}
/>
),
key: "remote",
},
{
component: (
<APIConfigSection
ref={apiSectionRef}
onChanged={markAsChanged}
onFocus={() => {
setCurrentFocusIndex(1);
setCurrentSection("api");
}}
/>
),
key: "api",
},
{
component: (
<LiveStreamSection
ref={liveStreamSectionRef}
onChanged={markAsChanged}
onFocus={() => {
setCurrentFocusIndex(2);
setCurrentSection("livestream");
}}
/>
),
key: "livestream",
},
// {
// component: (
// <VideoSourceSection
// onChanged={markAsChanged}
// onFocus={() => {
// setCurrentFocusIndex(3);
// setCurrentSection("videoSource");
// }}
// />
// ),
// key: "videoSource",
// },
Platform.OS === "android" && {
component: <UpdateSection />,
key: "update",
},
].filter(Boolean);
// TV遥控器事件处理 - 仅在TV设备上启用
const handleTVEvent = React.useCallback(
(event: any) => {
if (deviceType !== "tv") return;
if (event.eventType === "down") {
const nextIndex = Math.min(currentFocusIndex + 1, sections.length);
setCurrentFocusIndex(nextIndex);
if (nextIndex === sections.length) {
saveButtonRef.current?.focus();
}
} else if (event.eventType === "up") {
const prevIndex = Math.max(currentFocusIndex - 1, 0);
setCurrentFocusIndex(prevIndex);
}
},
[currentFocusIndex, sections.length, deviceType]
);
useTVEventHandler(deviceType === "tv" ? handleTVEvent : () => {});
// 动态样式
const dynamicStyles = createResponsiveStyles(deviceType, spacing);
const renderSettingsContent = () => (
<KeyboardAvoidingView style={{ flex: 1, backgroundColor }} behavior={Platform.OS === "ios" ? "padding" : "height"}>
<ThemedView style={[commonStyles.container, dynamicStyles.container]}>
{deviceType === "tv" && (
<View style={dynamicStyles.header}>
<ThemedText style={dynamicStyles.title}></ThemedText>
</View>
)}
<View style={dynamicStyles.scrollView}>
<FlatList
data={sections}
renderItem={({ item }) => {
if (item) {
return item.component;
}
return null;
}}
keyExtractor={(item) => (item ? item.key : "default")}
showsVerticalScrollIndicator={false}
contentContainerStyle={dynamicStyles.listContent}
/>
</View>
<View style={dynamicStyles.footer}>
<StyledButton
text={isLoading ? "保存中..." : "保存设置"}
onPress={handleSave}
variant="primary"
disabled={!hasChanges || isLoading}
style={[dynamicStyles.saveButton, (!hasChanges || isLoading) && dynamicStyles.disabledButton]}
/>
</View>
</ThemedView>
</KeyboardAvoidingView>
);
// 根据设备类型决定是否包装在响应式导航中
if (deviceType === "tv") {
return renderSettingsContent();
}
return (
<ResponsiveNavigation>
<ResponsiveHeader title="设置" showBackButton />
{renderSettingsContent()}
</ResponsiveNavigation>
);
}
const createResponsiveStyles = (deviceType: string, spacing: number) => {
const isMobile = deviceType === "mobile";
const isTablet = deviceType === "tablet";
const isTV = deviceType === "tv";
const minTouchTarget = DeviceUtils.getMinTouchTargetSize();
return StyleSheet.create({
container: {
flex: 1,
padding: spacing,
paddingTop: isTV ? spacing * 2 : 0,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing,
},
title: {
fontSize: isMobile ? 24 : isTablet ? 28 : 32,
fontWeight: "bold",
paddingTop: spacing,
color: "white",
},
scrollView: {
flex: 1,
},
listContent: {
paddingBottom: spacing,
},
footer: {
paddingTop: spacing,
alignItems: isMobile ? "center" : "flex-end",
},
saveButton: {
minHeight: isMobile ? minTouchTarget : isTablet ? 50 : 50,
width: isMobile ? "100%" : isTablet ? 140 : 120,
maxWidth: isMobile ? 280 : undefined,
},
disabledButton: {
opacity: 0.5,
},
});
};