diff --git a/app/detail.tsx b/app/detail.tsx index 4ce1f08..0df4093 100644 --- a/app/detail.tsx +++ b/app/detail.tsx @@ -6,6 +6,7 @@ import { ThemedText } from "@/components/ThemedText"; import { api, SearchResult } from "@/services/api"; import { getResolutionFromM3U8 } from "@/services/m3u8"; import { StyledButton } from "@/components/StyledButton"; +import { useSettingsStore } from "@/stores/settingsStore"; export default function DetailScreen() { const { source, q } = useLocalSearchParams(); @@ -16,6 +17,7 @@ export default function DetailScreen() { const [error, setError] = useState(null); const [allSourcesLoaded, setAllSourcesLoaded] = useState(false); const controllerRef = useRef(null); + const { videoSource } = useSettingsStore(); useEffect(() => { if (controllerRef.current) { @@ -33,13 +35,24 @@ export default function DetailScreen() { setAllSourcesLoaded(false); try { - const resources = await api.getResources(signal); - if (!resources || resources.length === 0) { + const allResources = await api.getResources(signal); + if (!allResources || allResources.length === 0) { setError("没有可用的播放源"); setLoading(false); return; } + // Filter resources based on enabled sources in settings + const resources = videoSource.enabledAll + ? allResources + : allResources.filter((resource) => videoSource.sources[resource.key]); + + if (!videoSource.enabledAll && resources.length === 0) { + setError("请到设置页面启用的播放源"); + setLoading(false); + return; + } + let foundFirstResult = false; // Prioritize source from params if available if (typeof source === "string") { @@ -102,7 +115,7 @@ export default function DetailScreen() { return () => { controllerRef.current?.abort(); }; - }, [q, source]); + }, [q, source, videoSource.enabledAll, videoSource.sources]); const handlePlay = (episodeName: string, episodeIndex: number) => { if (!detail) return; @@ -131,7 +144,9 @@ export default function DetailScreen() { if (error) { return ( - {error} + + {error} + ); } @@ -222,6 +237,10 @@ const styles = StyleSheet.create({ flexDirection: "row", padding: 20, }, + text: { + padding: 20, + textAlign: "center", + }, poster: { width: 200, height: 300, diff --git a/app/live.tsx b/app/live.tsx index 2c61798..c43c2dc 100644 --- a/app/live.tsx +++ b/app/live.tsx @@ -5,10 +5,10 @@ import { fetchAndParseM3u, getPlayableUrl, Channel } from "@/services/m3u"; import { ThemedView } from "@/components/ThemedView"; import { StyledButton } from "@/components/StyledButton"; import { AVPlaybackStatus } from "expo-av"; - -const M3U_URL = "https://raw.githubusercontent.com/sjnhnp/adblock/refs/heads/main/filtered_http_only_valid.m3u"; +import { useSettingsStore } from "@/stores/settingsStore"; export default function LiveScreen() { + const { m3uUrl } = useSettingsStore(); const [channels, setChannels] = useState([]); const [groupedChannels, setGroupedChannels] = useState>({}); const [channelGroups, setChannelGroups] = useState([]); @@ -24,8 +24,9 @@ export default function LiveScreen() { useEffect(() => { const loadChannels = async () => { + if (!m3uUrl) return; setIsLoading(true); - const parsedChannels = await fetchAndParseM3u(M3U_URL); + const parsedChannels = await fetchAndParseM3u(m3uUrl); setChannels(parsedChannels); const groups: Record = parsedChannels.reduce((acc, channel) => { @@ -48,7 +49,7 @@ export default function LiveScreen() { setIsLoading(false); }; loadChannels(); - }, []); + }, [m3uUrl]); const showChannelTitle = (title: string) => { setChannelTitle(title); diff --git a/app/settings.tsx b/app/settings.tsx index a411d0a..2922af9 100644 --- a/app/settings.tsx +++ b/app/settings.tsx @@ -1,19 +1,25 @@ -import React, { useState, useEffect } from "react"; -import { View, StyleSheet, ScrollView, Alert } from "react-native"; +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 { APIConfigSection } from "@/components/settings/APIConfigSection"; import { LiveStreamSection } from "@/components/settings/LiveStreamSection"; import { RemoteInputSection } from "@/components/settings/RemoteInputSection"; -import { PlaySourceSection } from "@/components/settings/PlaybackSourceSection"; +import { VideoSourceSection } from "@/components/settings/VideoSourceSection"; export default function SettingsScreen() { const { loadSettings, saveSettings } = useSettingsStore(); + const backgroundColor = useThemeColor({}, "background"); const [hasChanges, setHasChanges] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [currentFocusIndex, setCurrentFocusIndex] = useState(0); + + const saveButtonRef = useRef(null); useEffect(() => { loadSettings(); @@ -24,7 +30,6 @@ export default function SettingsScreen() { try { await saveSettings(); setHasChanges(false); - Alert.alert("成功", "设置已保存"); } catch { Alert.alert("错误", "保存设置失败"); } finally { @@ -36,36 +41,78 @@ export default function SettingsScreen() { setHasChanges(true); }; + const sections = [ + { + component: setCurrentFocusIndex(0)} />, + key: "remote", + }, + { + component: setCurrentFocusIndex(1)} />, + key: "api", + }, + { + component: setCurrentFocusIndex(2)} />, + key: "livestream", + }, + { + component: setCurrentFocusIndex(3)} />, + key: "playback", + }, + ]; + + // TV遥控器事件处理 + const handleTVEvent = React.useCallback( + (event: any) => { + 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] + ); + + useTVEventHandler(handleTVEvent); + return ( - - - 设置 - + + + + 设置 + - - - - - - + + item.component} + keyExtractor={(item) => item.key} + showsVerticalScrollIndicator={false} + /> + - - - - + + + + + ); } const styles = StyleSheet.create({ container: { flex: 1, - padding: 24, + padding: 12, }, header: { flexDirection: "row", @@ -85,12 +132,12 @@ const styles = StyleSheet.create({ flex: 1, }, footer: { - paddingTop: 24, - borderTopWidth: 1, - borderTopColor: "#333", + paddingTop: 12, + alignItems: "flex-end", }, saveButton: { minHeight: 50, + width: 120, }, disabledButton: { opacity: 0.5, diff --git a/components/StyledButton.tsx b/components/StyledButton.tsx index 751fb8a..61b0abd 100644 --- a/components/StyledButton.tsx +++ b/components/StyledButton.tsx @@ -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; diff --git a/components/settings/APIConfigSection.tsx b/components/settings/APIConfigSection.tsx index 5d22a30..55a4c6a 100644 --- a/components/settings/APIConfigSection.tsx +++ b/components/settings/APIConfigSection.tsx @@ -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 = ({ onChanged }) => { +export const APIConfigSection: React.FC = ({ onChanged, onFocus, onBlur }) => { const { apiBaseUrl, setApiBaseUrl } = useSettingsStore(); const [isInputFocused, setIsInputFocused] = useState(false); + const [isSectionFocused, setIsSectionFocused] = useState(false); const inputRef = useRef(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 ( - + API 地址 - setIsInputFocused(true)} - onBlur={() => setIsInputFocused(false)} - /> + + setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + /> + - + ); }; const styles = StyleSheet.create({ - section: { - padding: 20, - marginBottom: 16, - borderRadius: 12, - borderWidth: 1, - borderColor: "#333", - }, sectionTitle: { fontSize: 16, fontWeight: "bold", diff --git a/components/settings/LiveStreamSection.tsx b/components/settings/LiveStreamSection.tsx index 98a396b..b271695 100644 --- a/components/settings/LiveStreamSection.tsx +++ b/components/settings/LiveStreamSection.tsx @@ -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 = ({ onChanged }) => { +export const LiveStreamSection: React.FC = ({ onChanged, onFocus, onBlur }) => { + const { m3uUrl, setM3uUrl } = useSettingsStore(); + const [isInputFocused, setIsInputFocused] = useState(false); + const [isSectionFocused, setIsSectionFocused] = useState(false); + const inputRef = useRef(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 ( - - 直播源配置 - 直播源配置功能即将上线 - + + + 直播源地址 + + setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + /> + + + ); }; 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, }, }); \ No newline at end of file diff --git a/components/settings/PlaybackSourceSection.tsx b/components/settings/PlaybackSourceSection.tsx deleted file mode 100644 index 8895461..0000000 --- a/components/settings/PlaybackSourceSection.tsx +++ /dev/null @@ -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 = ({ onChanged }) => { - return ( - - 播放源配置 - 播放源配置功能即将上线 - - ); -}; - -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", - }, -}); diff --git a/components/settings/RemoteInputSection.tsx b/components/settings/RemoteInputSection.tsx index 7e59d7b..82e33fc 100644 --- a/components/settings/RemoteInputSection.tsx +++ b/components/settings/RemoteInputSection.tsx @@ -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 = ({ onChanged }) => { +export const RemoteInputSection: React.FC = ({ 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 ( - - + + 启用远程输入 - - + + {}} // 禁用Switch的直接交互 + trackColor={{ false: "#767577", true: "#007AFF" }} + thumbColor={remoteInputEnabled ? "#ffffff" : "#f4f3f4"} + pointerEvents="none" + /> + + {remoteInputEnabled && ( @@ -56,23 +90,11 @@ export const RemoteInputSection: React.FC = ({ onChange )} )} - + ); }; 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", diff --git a/components/settings/SettingsSection.tsx b/components/settings/SettingsSection.tsx new file mode 100644 index 0000000..1e38403 --- /dev/null +++ b/components/settings/SettingsSection.tsx @@ -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 = ({ + children, + onFocus, + onBlur, + focusable = false +}) => { + const [isFocused, setIsFocused] = useState(false); + + const handleFocus = () => { + setIsFocused(true); + onFocus?.(); + }; + + const handleBlur = () => { + setIsFocused(false); + onBlur?.(); + }; + + if (!focusable) { + return ( + + {children} + + ); + } + + return ( + + + {children} + + + ); +}; + +const styles = StyleSheet.create({ + section: { + padding: 20, + marginBottom: 16, + borderRadius: 12, + borderWidth: 1, + borderColor: "#333", + }, + sectionFocused: { + borderColor: "#007AFF", + backgroundColor: "#007AFF10", + }, + sectionPressable: { + width: "100%", + }, +}); \ No newline at end of file diff --git a/components/settings/VideoSourceSection.tsx b/components/settings/VideoSourceSection.tsx new file mode 100644 index 0000000..a15dd4f --- /dev/null +++ b/components/settings/VideoSourceSection.tsx @@ -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 = ({ onChanged, onFocus, onBlur }) => { + const [resources, setResources] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [focusedIndex, setFocusedIndex] = useState(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 ( + + setFocusedIndex(index)} + onBlur={() => setFocusedIndex(null)} + > + {item.name} + {}} // 禁用Switch的直接交互 + trackColor={{ false: "#767577", true: "#007AFF" }} + thumbColor={isEnabled ? "#ffffff" : "#f4f3f4"} + pointerEvents="none" + /> + + + ); + }; + + return ( + + 播放源配置 + + {loading && ( + + + 加载中... + + )} + + {error && {error}} + + {!loading && !error && resources.length > 0 && ( + item.key} + numColumns={3} + columnWrapperStyle={styles.row} + contentContainerStyle={styles.flatListContainer} + scrollEnabled={false} + /> + )} + + ); +}; + +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, + }, +}); diff --git a/docs/SETTINGS_REFACTOR_PLAN.md b/docs/SETTINGS_REFACTOR_PLAN.md index f1a0a02..c83c30c 100644 --- a/docs/SETTINGS_REFACTOR_PLAN.md +++ b/docs/SETTINGS_REFACTOR_PLAN.md @@ -34,7 +34,7 @@ interface SettingsState { // 新增配置项 liveStreamSources: LiveStreamSource[]; // 直播源配置 remoteInputEnabled: boolean; // 远程输入开关 - playbackSourceConfig: PlaybackSourceConfig; // 播放源配置 + videoSourceConfig: VideoSourceConfig; // 播放源配置 } interface LiveStreamSource { @@ -44,7 +44,7 @@ interface LiveStreamSource { enabled: boolean; } -interface PlaybackSourceConfig { +interface VideoSourceConfig { primarySource: string; fallbackSources: string[]; enabledSources: string[]; diff --git a/hooks/useButtonAnimation.ts b/hooks/useAnimation.ts similarity index 67% rename from hooks/useButtonAnimation.ts rename to hooks/useAnimation.ts index 6649647..15d02f8 100644 --- a/hooks/useButtonAnimation.ts +++ b/hooks/useAnimation.ts @@ -1,16 +1,16 @@ import { useRef, useEffect } from 'react'; import { Animated } from 'react-native'; -export const useButtonAnimation = (isFocused: boolean) => { +export const useButtonAnimation = (isFocused: boolean, size: number = 1.1) => { const scaleValue = useRef(new Animated.Value(1)).current; useEffect(() => { Animated.spring(scaleValue, { - toValue: isFocused ? 1.1 : 1, + toValue: isFocused ? size : 1, friction: 5, useNativeDriver: true, }).start(); - }, [ isFocused, scaleValue]); + }, [ isFocused, scaleValue, size]); return { transform: [{ scale: scaleValue }], diff --git a/services/storage.ts b/services/storage.ts index c1f3df7..c8895b5 100644 --- a/services/storage.ts +++ b/services/storage.ts @@ -25,10 +25,15 @@ export interface FavoriteItem { } export interface AppSettings { - theme: "light" | "dark" | "auto"; - autoPlay: boolean; - playbackSpeed: number; apiBaseUrl: string; + remoteInputEnabled: boolean; + videoSource: { + enabledAll: boolean; + sources: { + [key: string]: boolean; + }; + }, + m3uUrl: string; } // --- Helper --- @@ -178,10 +183,13 @@ export class SearchHistoryManager { export class SettingsManager { static async get(): Promise { const defaultSettings: AppSettings = { - theme: "auto", - autoPlay: true, - playbackSpeed: 1.0, - apiBaseUrl: "", + apiBaseUrl: "https://orion-tv.edu.deal", + remoteInputEnabled: false, + videoSource: { + enabledAll: true, + sources: {}, + }, + m3uUrl: "https://raw.githubusercontent.com/sjnhnp/adblock/refs/heads/main/filtered_http_only_valid.m3u", }; try { const data = await AsyncStorage.getItem(STORAGE_KEYS.SETTINGS); diff --git a/stores/settingsStore.ts b/stores/settingsStore.ts index 4d6ba0e..a0e4d42 100644 --- a/stores/settingsStore.ts +++ b/stores/settingsStore.ts @@ -3,93 +3,62 @@ import { SettingsManager } from '@/services/storage'; import { api } from '@/services/api'; import useHomeStore from './homeStore'; -export interface LiveStreamSource { - id: string; - name: string; - url: string; - enabled: boolean; -} - -export interface PlaybackSourceConfig { - primarySource: string; - fallbackSources: string[]; - enabledSources: string[]; -} interface SettingsState { apiBaseUrl: string; - liveStreamSources: LiveStreamSource[]; + m3uUrl: string; remoteInputEnabled: boolean; - playbackSourceConfig: PlaybackSourceConfig; + videoSource: { + enabledAll: boolean; + sources: { + [key: string]: boolean; + }; + }; isModalVisible: boolean; loadSettings: () => Promise; setApiBaseUrl: (url: string) => void; - setLiveStreamSources: (sources: LiveStreamSource[]) => void; - addLiveStreamSource: (source: Omit) => void; - removeLiveStreamSource: (id: string) => void; - updateLiveStreamSource: (id: string, updates: Partial) => void; + setM3uUrl: (url: string) => void; setRemoteInputEnabled: (enabled: boolean) => void; - setPlaybackSourceConfig: (config: PlaybackSourceConfig) => void; saveSettings: () => Promise; + setVideoSource: (config: { enabledAll: boolean; sources: {[key: string]: boolean} }) => void; showModal: () => void; hideModal: () => void; } export const useSettingsStore = create((set, get) => ({ - apiBaseUrl: 'https://orion-tv.edu.deal', + apiBaseUrl: '', + m3uUrl: 'https://raw.githubusercontent.com/sjnhnp/adblock/refs/heads/main/filtered_http_only_valid.m3u', liveStreamSources: [], remoteInputEnabled: false, - playbackSourceConfig: { - primarySource: 'default', - fallbackSources: [], - enabledSources: ['default'], - }, isModalVisible: false, + videoSource: { + enabledAll: true, + sources: {}, + }, loadSettings: async () => { const settings = await SettingsManager.get(); set({ apiBaseUrl: settings.apiBaseUrl, - liveStreamSources: settings.liveStreamSources || [], + m3uUrl: settings.m3uUrl, remoteInputEnabled: settings.remoteInputEnabled || false, - playbackSourceConfig: settings.playbackSourceConfig || { - primarySource: 'default', - fallbackSources: [], - enabledSources: ['default'], + videoSource: settings.videoSource || { + enabledAll: true, + sources: {}, }, }); api.setBaseUrl(settings.apiBaseUrl); }, setApiBaseUrl: (url) => set({ apiBaseUrl: url }), - setLiveStreamSources: (sources) => set({ liveStreamSources: sources }), - addLiveStreamSource: (source) => { - const { liveStreamSources } = get(); - const newSource = { - ...source, - id: Date.now().toString(), - }; - set({ liveStreamSources: [...liveStreamSources, newSource] }); - }, - removeLiveStreamSource: (id) => { - const { liveStreamSources } = get(); - set({ liveStreamSources: liveStreamSources.filter(s => s.id !== id) }); - }, - updateLiveStreamSource: (id, updates) => { - const { liveStreamSources } = get(); - set({ - liveStreamSources: liveStreamSources.map(s => - s.id === id ? { ...s, ...updates } : s - ) - }); - }, + setM3uUrl: (url) => set({ m3uUrl: url }), setRemoteInputEnabled: (enabled) => set({ remoteInputEnabled: enabled }), - setPlaybackSourceConfig: (config) => set({ playbackSourceConfig: config }), + setVideoSource: (config) => set({ videoSource: config }), saveSettings: async () => { - const { apiBaseUrl, liveStreamSources, remoteInputEnabled, playbackSourceConfig } = get(); + const { apiBaseUrl, m3uUrl, remoteInputEnabled, videoSource } = get(); await SettingsManager.save({ apiBaseUrl, - liveStreamSources, + m3uUrl, remoteInputEnabled, - playbackSourceConfig, + videoSource, }); api.setBaseUrl(apiBaseUrl); set({ isModalVisible: false });