mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-04 03:36:29 +08:00
Add SafeAreaProvider to root layout and implement proper safe area handling: - Wrap app in SafeAreaProvider in _layout.tsx - Update HomeScreen to use safe area insets for proper top padding - Fix SettingsScreen safe area handling for all device types - Update ResponsiveHeader to use SafeAreaContext instead of manual calculation This ensures content is not covered by the status bar on mobile and tablet devices while maintaining TV compatibility.
263 lines
8.2 KiB
TypeScript
263 lines
8.2 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 { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
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 insets = useSafeAreaInsets();
|
|
|
|
// 响应式布局配置
|
|
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, insets);
|
|
|
|
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, insets: any) => {
|
|
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 : isMobile ? insets.top + spacing : insets.top + spacing * 1.5,
|
|
},
|
|
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,
|
|
},
|
|
});
|
|
};
|