mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-04 03:36:29 +08:00
feat: Refactor settings management into a dedicated page with new configuration options, including live stream source and remote input settings
This commit is contained in:
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { Animated, Pressable, StyleSheet, StyleProp, ViewStyle, PressableProps, TextStyle } from "react-native";
|
||||
import { ThemedText } from "./ThemedText";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { useButtonAnimation } from "@/hooks/useButtonAnimation";
|
||||
import { useButtonAnimation } from "@/hooks/useAnimation";
|
||||
|
||||
interface StyledButtonProps extends PressableProps {
|
||||
children?: React.ReactNode;
|
||||
|
||||
@@ -1,52 +1,75 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { View, TextInput, StyleSheet } from "react-native";
|
||||
import { View, TextInput, StyleSheet, Pressable, Animated } from "react-native";
|
||||
import { useTVEventHandler } from "react-native";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { ThemedView } from "@/components/ThemedView";
|
||||
import { SettingsSection } from "./SettingsSection";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useButtonAnimation } from "@/hooks/useAnimation";
|
||||
|
||||
interface APIConfigSectionProps {
|
||||
onChanged: () => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
export const APIConfigSection: React.FC<APIConfigSectionProps> = ({ onChanged }) => {
|
||||
export const APIConfigSection: React.FC<APIConfigSectionProps> = ({ onChanged, onFocus, onBlur }) => {
|
||||
const { apiBaseUrl, setApiBaseUrl } = useSettingsStore();
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
const [isSectionFocused, setIsSectionFocused] = useState(false);
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const inputAnimationStyle = useButtonAnimation(isSectionFocused, 1.01);
|
||||
|
||||
const handleUrlChange = (url: string) => {
|
||||
setApiBaseUrl(url);
|
||||
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]
|
||||
);
|
||||
|
||||
useTVEventHandler(handleTVEvent);
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.section}>
|
||||
<SettingsSection focusable onFocus={handleSectionFocus} onBlur={handleSectionBlur}>
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.sectionTitle}>API 地址</ThemedText>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
style={[styles.input, isInputFocused && styles.inputFocused]}
|
||||
value={apiBaseUrl}
|
||||
onChangeText={handleUrlChange}
|
||||
placeholder="输入 API 地址"
|
||||
placeholderTextColor="#888"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
/>
|
||||
<Animated.View style={inputAnimationStyle}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
style={[styles.input, isInputFocused && styles.inputFocused]}
|
||||
value={apiBaseUrl}
|
||||
onChangeText={handleUrlChange}
|
||||
placeholder="输入 API 地址"
|
||||
placeholderTextColor="#888"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</ThemedView>
|
||||
</SettingsSection>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: {
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: "#333",
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
|
||||
@@ -1,37 +1,98 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { View, TextInput, StyleSheet, Animated } from 'react-native';
|
||||
import { useTVEventHandler } from 'react-native';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { SettingsSection } from './SettingsSection';
|
||||
import { useSettingsStore } from '@/stores/settingsStore';
|
||||
import { useButtonAnimation } from '@/hooks/useAnimation';
|
||||
|
||||
interface LiveStreamSectionProps {
|
||||
onChanged: () => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
export const LiveStreamSection: React.FC<LiveStreamSectionProps> = ({ onChanged }) => {
|
||||
export const LiveStreamSection: React.FC<LiveStreamSectionProps> = ({ onChanged, onFocus, onBlur }) => {
|
||||
const { m3uUrl, setM3uUrl } = useSettingsStore();
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
const [isSectionFocused, setIsSectionFocused] = useState(false);
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const inputAnimationStyle = useButtonAnimation(isSectionFocused, 1.01);
|
||||
|
||||
const handleUrlChange = (url: string) => {
|
||||
setM3uUrl(url);
|
||||
onChanged();
|
||||
};
|
||||
|
||||
const handleSectionFocus = () => {
|
||||
setIsSectionFocused(true);
|
||||
onFocus?.();
|
||||
};
|
||||
|
||||
const handleSectionBlur = () => {
|
||||
setIsSectionFocused(false);
|
||||
onBlur?.();
|
||||
};
|
||||
|
||||
const handleTVEvent = React.useCallback(
|
||||
(event: any) => {
|
||||
if (isSectionFocused && event.eventType === "select") {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
},
|
||||
[isSectionFocused]
|
||||
);
|
||||
|
||||
useTVEventHandler(handleTVEvent);
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>直播源配置</ThemedText>
|
||||
<ThemedText style={styles.placeholder}>直播源配置功能即将上线</ThemedText>
|
||||
</ThemedView>
|
||||
<SettingsSection focusable onFocus={handleSectionFocus} onBlur={handleSectionBlur}>
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.sectionTitle}>直播源地址</ThemedText>
|
||||
<Animated.View style={inputAnimationStyle}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
style={[styles.input, isInputFocused && styles.inputFocused]}
|
||||
value={m3uUrl}
|
||||
onChangeText={handleUrlChange}
|
||||
placeholder="输入 M3U 直播源地址"
|
||||
placeholderTextColor="#888"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</SettingsSection>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: {
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
placeholder: {
|
||||
fontSize: 14,
|
||||
color: '#888',
|
||||
fontStyle: 'italic',
|
||||
inputContainer: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
input: {
|
||||
height: 50,
|
||||
borderWidth: 2,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 15,
|
||||
fontSize: 16,
|
||||
backgroundColor: '#3a3a3c',
|
||||
color: 'white',
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
inputFocused: {
|
||||
borderColor: '#007AFF',
|
||||
shadowColor: '#007AFF',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.8,
|
||||
shadowRadius: 10,
|
||||
elevation: 5,
|
||||
},
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { ThemedView } from "@/components/ThemedView";
|
||||
|
||||
interface PlaybackSourceSectionProps {
|
||||
onChanged: () => void;
|
||||
}
|
||||
|
||||
export const PlaySourceSection: React.FC<PlaybackSourceSectionProps> = ({ onChanged }) => {
|
||||
return (
|
||||
<ThemedView style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>播放源配置</ThemedText>
|
||||
<ThemedText style={styles.placeholder}>播放源配置功能即将上线</ThemedText>
|
||||
</ThemedView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: {
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: "#333",
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 16,
|
||||
},
|
||||
placeholder: {
|
||||
fontSize: 14,
|
||||
color: "#888",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
});
|
||||
@@ -1,36 +1,70 @@
|
||||
import React from "react";
|
||||
import { View, Switch, StyleSheet } from "react-native";
|
||||
import React, { useCallback } from "react";
|
||||
import { View, Switch, StyleSheet, Pressable, Animated } from "react-native";
|
||||
import { useTVEventHandler } from "react-native";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { ThemedView } from "@/components/ThemedView";
|
||||
import { SettingsSection } from "./SettingsSection";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useRemoteControlStore } from "@/stores/remoteControlStore";
|
||||
import { useButtonAnimation } from "@/hooks/useAnimation";
|
||||
|
||||
interface RemoteInputSectionProps {
|
||||
onChanged: () => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
export const RemoteInputSection: React.FC<RemoteInputSectionProps> = ({ onChanged }) => {
|
||||
export const RemoteInputSection: React.FC<RemoteInputSectionProps> = ({ onChanged, onFocus, onBlur }) => {
|
||||
const { remoteInputEnabled, setRemoteInputEnabled } = useSettingsStore();
|
||||
const { isServerRunning, serverUrl, error } = useRemoteControlStore();
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
const animationStyle = useButtonAnimation(isFocused, 1.2);
|
||||
|
||||
const handleToggle = async (enabled: boolean) => {
|
||||
setRemoteInputEnabled(enabled);
|
||||
onChanged();
|
||||
const handleToggle = useCallback(
|
||||
(enabled: boolean) => {
|
||||
setRemoteInputEnabled(enabled);
|
||||
onChanged();
|
||||
},
|
||||
[setRemoteInputEnabled, onChanged]
|
||||
);
|
||||
|
||||
const handleSectionFocus = () => {
|
||||
setIsFocused(true);
|
||||
onFocus?.();
|
||||
};
|
||||
|
||||
const handleSectionBlur = () => {
|
||||
setIsFocused(false);
|
||||
onBlur?.();
|
||||
};
|
||||
|
||||
// TV遥控器事件处理
|
||||
const handleTVEvent = React.useCallback(
|
||||
(event: any) => {
|
||||
if (isFocused && event.eventType === "select") {
|
||||
handleToggle(!remoteInputEnabled);
|
||||
}
|
||||
},
|
||||
[isFocused, remoteInputEnabled, handleToggle]
|
||||
);
|
||||
|
||||
useTVEventHandler(handleTVEvent);
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.section}>
|
||||
<View style={styles.settingItem}>
|
||||
<SettingsSection focusable onFocus={handleSectionFocus} onBlur={handleSectionBlur}>
|
||||
<Pressable style={styles.settingItem} onFocus={handleSectionFocus} onBlur={handleSectionBlur}>
|
||||
<View style={styles.settingInfo}>
|
||||
<ThemedText style={styles.settingName}>启用远程输入</ThemedText>
|
||||
</View>
|
||||
<Switch
|
||||
value={remoteInputEnabled}
|
||||
onValueChange={handleToggle}
|
||||
trackColor={{ false: "#767577", true: "#007AFF" }}
|
||||
thumbColor={remoteInputEnabled ? "#ffffff" : "#f4f3f4"}
|
||||
/>
|
||||
</View>
|
||||
<Animated.View style={animationStyle}>
|
||||
<Switch
|
||||
value={remoteInputEnabled}
|
||||
onValueChange={() => {}} // 禁用Switch的直接交互
|
||||
trackColor={{ false: "#767577", true: "#007AFF" }}
|
||||
thumbColor={remoteInputEnabled ? "#ffffff" : "#f4f3f4"}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
|
||||
{remoteInputEnabled && (
|
||||
<View style={styles.statusContainer}>
|
||||
@@ -56,23 +90,11 @@ export const RemoteInputSection: React.FC<RemoteInputSectionProps> = ({ onChange
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ThemedView>
|
||||
</SettingsSection>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: {
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: "#333",
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 16,
|
||||
},
|
||||
settingItem: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
|
||||
66
components/settings/SettingsSection.tsx
Normal file
66
components/settings/SettingsSection.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, Pressable } from "react-native";
|
||||
import { ThemedView } from "@/components/ThemedView";
|
||||
|
||||
interface SettingsSectionProps {
|
||||
children: React.ReactNode;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
focusable?: boolean;
|
||||
}
|
||||
|
||||
export const SettingsSection: React.FC<SettingsSectionProps> = ({
|
||||
children,
|
||||
onFocus,
|
||||
onBlur,
|
||||
focusable = false
|
||||
}) => {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsFocused(true);
|
||||
onFocus?.();
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsFocused(false);
|
||||
onBlur?.();
|
||||
};
|
||||
|
||||
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}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
</ThemedView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: {
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: "#333",
|
||||
},
|
||||
sectionFocused: {
|
||||
borderColor: "#007AFF",
|
||||
backgroundColor: "#007AFF10",
|
||||
},
|
||||
sectionPressable: {
|
||||
width: "100%",
|
||||
},
|
||||
});
|
||||
213
components/settings/VideoSourceSection.tsx
Normal file
213
components/settings/VideoSourceSection.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { StyleSheet, View, Switch, ActivityIndicator, FlatList, Pressable, Animated } from "react-native";
|
||||
import { useTVEventHandler } from "react-native";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { SettingsSection } from "./SettingsSection";
|
||||
import { api, ApiSite } from "@/services/api";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
|
||||
interface VideoSourceSectionProps {
|
||||
onChanged: () => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
export const VideoSourceSection: React.FC<VideoSourceSectionProps> = ({ onChanged, onFocus, onBlur }) => {
|
||||
const [resources, setResources] = useState<ApiSite[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
const [isSectionFocused, setIsSectionFocused] = useState(false);
|
||||
const { videoSource, setVideoSource } = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
fetchResources();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const fetchResources = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const resourcesList = await api.getResources();
|
||||
setResources(resourcesList);
|
||||
|
||||
if (videoSource.enabledAll && resourcesList.length === 0) {
|
||||
const allResourceKeys: { [key: string]: boolean } = {};
|
||||
for (const resource of resourcesList) {
|
||||
allResourceKeys[resource.key] = true;
|
||||
}
|
||||
setVideoSource({
|
||||
enabledAll: true,
|
||||
sources: allResourceKeys,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setError("获取播放源失败");
|
||||
console.error("Failed to fetch resources:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleResourceEnabled = useCallback(
|
||||
(resourceKey: string) => {
|
||||
const isEnabled = videoSource.sources[resourceKey];
|
||||
|
||||
const newEnabledSources = { ...videoSource.sources, [resourceKey]: !isEnabled };
|
||||
|
||||
setVideoSource({
|
||||
enabledAll: Object.values(newEnabledSources).every((enabled) => enabled),
|
||||
sources: newEnabledSources,
|
||||
});
|
||||
|
||||
onChanged();
|
||||
},
|
||||
[videoSource.sources, setVideoSource, onChanged]
|
||||
);
|
||||
|
||||
const handleSectionFocus = () => {
|
||||
setIsSectionFocused(true);
|
||||
onFocus?.();
|
||||
};
|
||||
|
||||
const handleSectionBlur = () => {
|
||||
setIsSectionFocused(false);
|
||||
setFocusedIndex(null);
|
||||
onBlur?.();
|
||||
};
|
||||
|
||||
// TV遥控器事件处理
|
||||
const handleTVEvent = useCallback(
|
||||
(event: any) => {
|
||||
if (event.eventType === "select") {
|
||||
if (focusedIndex !== null) {
|
||||
const resource = resources[focusedIndex];
|
||||
if (resource) {
|
||||
toggleResourceEnabled(resource.key);
|
||||
}
|
||||
} else if (isSectionFocused) {
|
||||
setFocusedIndex(0);
|
||||
}
|
||||
}
|
||||
},
|
||||
[isSectionFocused, focusedIndex, resources, toggleResourceEnabled]
|
||||
);
|
||||
|
||||
useTVEventHandler(handleTVEvent);
|
||||
|
||||
const renderResourceItem = ({ item, index }: { item: ApiSite; index: number }) => {
|
||||
const isEnabled = videoSource.enabledAll || videoSource.sources[item.key];
|
||||
const isFocused = focusedIndex === index;
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.resourceItem]}>
|
||||
<Pressable
|
||||
hasTVPreferredFocus={isFocused}
|
||||
style={[styles.resourcePressable, isFocused && styles.resourceFocused]}
|
||||
onFocus={() => setFocusedIndex(index)}
|
||||
onBlur={() => setFocusedIndex(null)}
|
||||
>
|
||||
<ThemedText style={styles.resourceName}>{item.name}</ThemedText>
|
||||
<Switch
|
||||
value={isEnabled}
|
||||
onValueChange={() => {}} // 禁用Switch的直接交互
|
||||
trackColor={{ false: "#767577", true: "#007AFF" }}
|
||||
thumbColor={isEnabled ? "#ffffff" : "#f4f3f4"}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsSection focusable onFocus={handleSectionFocus} onBlur={handleSectionBlur}>
|
||||
<ThemedText style={styles.sectionTitle}>播放源配置</ThemedText>
|
||||
|
||||
{loading && (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" />
|
||||
<ThemedText style={styles.loadingText}>加载中...</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{error && <ThemedText style={styles.errorText}>{error}</ThemedText>}
|
||||
|
||||
{!loading && !error && resources.length > 0 && (
|
||||
<FlatList
|
||||
data={resources}
|
||||
renderItem={renderResourceItem}
|
||||
keyExtractor={(item) => item.key}
|
||||
numColumns={3}
|
||||
columnWrapperStyle={styles.row}
|
||||
contentContainerStyle={styles.flatListContainer}
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
)}
|
||||
</SettingsSection>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 16,
|
||||
},
|
||||
loadingContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 16,
|
||||
},
|
||||
loadingText: {
|
||||
marginLeft: 8,
|
||||
color: "#888",
|
||||
},
|
||||
errorText: {
|
||||
color: "#ff4444",
|
||||
fontSize: 14,
|
||||
textAlign: "center",
|
||||
padding: 16,
|
||||
},
|
||||
flatListContainer: {
|
||||
gap: 12,
|
||||
},
|
||||
row: {
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
resourceItem: {
|
||||
width: "32%",
|
||||
marginHorizontal: 6,
|
||||
marginVertical: 6,
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
resourcePressable: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: "#2a2a2a",
|
||||
borderRadius: 8,
|
||||
minHeight: 56,
|
||||
},
|
||||
resourceFocused: {
|
||||
backgroundColor: "#3a3a3c",
|
||||
borderWidth: 2,
|
||||
borderColor: "#007AFF",
|
||||
shadowColor: "#007AFF",
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.8,
|
||||
shadowRadius: 10,
|
||||
elevation: 5,
|
||||
},
|
||||
resourceName: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
flex: 1,
|
||||
marginRight: 8,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user