mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-15 20:34:43 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10bfbbbf8e | ||
|
|
187a753735 | ||
|
|
8cda0d7a82 | ||
|
|
b2de622a40 | ||
|
|
2988dad829 |
@@ -66,14 +66,10 @@ yarn ios-tv
|
|||||||
yarn android-tv
|
yarn android-tv
|
||||||
```
|
```
|
||||||
|
|
||||||
## 部署
|
## 使用
|
||||||
|
|
||||||
- 1.2.x 以上版本需配合 [MoonTV](https://github.com/senshinya/MoonTV) 使用。
|
- 1.2.x 以上版本需配合 [MoonTV](https://github.com/senshinya/MoonTV) 使用。
|
||||||
|
|
||||||
## 其他
|
|
||||||
|
|
||||||
- 最低版本是 android 6.0,可用,但是不推荐
|
|
||||||
- 如果使用 https 的后端接口无法访问,在确认服务没有问题的情况下,请检查 https 的 TLS 协议,Android 10 之后版本才支持 TLS1.3
|
|
||||||
|
|
||||||
## 📜 主要脚本
|
## 📜 主要脚本
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useCallback, useRef, useState } from "react";
|
import React, { useEffect, useCallback, useRef, useState } from "react";
|
||||||
import { View, StyleSheet, ActivityIndicator, FlatList, Pressable, Dimensions } from "react-native";
|
import { View, StyleSheet, ActivityIndicator, FlatList, Pressable, Dimensions, Animated } from "react-native";
|
||||||
import { ThemedView } from "@/components/ThemedView";
|
import { ThemedView } from "@/components/ThemedView";
|
||||||
import { ThemedText } from "@/components/ThemedText";
|
import { ThemedText } from "@/components/ThemedText";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
@@ -21,6 +21,7 @@ export default function HomeScreen() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const colorScheme = "dark";
|
const colorScheme = "dark";
|
||||||
const [selectedTag, setSelectedTag] = useState<string | null>(null);
|
const [selectedTag, setSelectedTag] = useState<string | null>(null);
|
||||||
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
categories,
|
categories,
|
||||||
@@ -59,6 +60,18 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
}, [fetchInitialData, selectedCategory, selectedCategory.tag]);
|
}, [fetchInitialData, selectedCategory, selectedCategory.tag]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && contentData.length > 0) {
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start();
|
||||||
|
} else if (loading) {
|
||||||
|
fadeAnim.setValue(0);
|
||||||
|
}
|
||||||
|
}, [loading, contentData.length, fadeAnim]);
|
||||||
|
|
||||||
const handleCategorySelect = (category: Category) => {
|
const handleCategorySelect = (category: Category) => {
|
||||||
setSelectedTag(null);
|
setSelectedTag(null);
|
||||||
selectCategory(category);
|
selectCategory(category);
|
||||||
@@ -196,18 +209,20 @@ export default function HomeScreen() {
|
|||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<CustomScrollView
|
<Animated.View style={[styles.contentContainer, { opacity: fadeAnim }]}>
|
||||||
data={contentData}
|
<CustomScrollView
|
||||||
renderItem={renderContentItem}
|
data={contentData}
|
||||||
numColumns={NUM_COLUMNS}
|
renderItem={renderContentItem}
|
||||||
loading={loading}
|
numColumns={NUM_COLUMNS}
|
||||||
loadingMore={loadingMore}
|
loading={loading}
|
||||||
error={error}
|
loadingMore={loadingMore}
|
||||||
onEndReached={loadMoreData}
|
error={error}
|
||||||
loadMoreThreshold={LOAD_MORE_THRESHOLD}
|
onEndReached={loadMoreData}
|
||||||
emptyMessage={selectedCategory?.tags ? "请选择一个子分类" : "该分类下暂无内容"}
|
loadMoreThreshold={LOAD_MORE_THRESHOLD}
|
||||||
ListFooterComponent={renderFooter}
|
emptyMessage={selectedCategory?.tags ? "请选择一个子分类" : "该分类下暂无内容"}
|
||||||
/>
|
ListFooterComponent={renderFooter}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
);
|
);
|
||||||
@@ -266,6 +281,9 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingBottom: 20,
|
paddingBottom: 20,
|
||||||
},
|
},
|
||||||
|
contentContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
itemContainer: {
|
itemContainer: {
|
||||||
margin: 8,
|
margin: 8,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const LoginModal = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoginModalVisible && !isSettingsPage) {
|
if (isLoginModalVisible && !isSettingsPage) {
|
||||||
const isUsernameVisible = serverConfig?.StorageType !== "localstorage";
|
const isUsernameVisible = serverConfig?.StorageType !== "localstorage";
|
||||||
|
|
||||||
// Use a small delay to ensure the modal is fully rendered
|
// Use a small delay to ensure the modal is fully rendered
|
||||||
const focusTimeout = setTimeout(() => {
|
const focusTimeout = setTimeout(() => {
|
||||||
if (isUsernameVisible) {
|
if (isUsernameVisible) {
|
||||||
@@ -35,7 +35,7 @@ const LoginModal = () => {
|
|||||||
passwordInputRef.current?.focus();
|
passwordInputRef.current?.focus();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return () => clearTimeout(focusTimeout);
|
return () => clearTimeout(focusTimeout);
|
||||||
}
|
}
|
||||||
}, [isLoginModalVisible, serverConfig, isSettingsPage]);
|
}, [isLoginModalVisible, serverConfig, isSettingsPage]);
|
||||||
@@ -55,15 +55,19 @@ const LoginModal = () => {
|
|||||||
hideLoginModal();
|
hideLoginModal();
|
||||||
setUsername("");
|
setUsername("");
|
||||||
setPassword("");
|
setPassword("");
|
||||||
|
|
||||||
// Show disclaimer alert after successful login
|
// Show disclaimer alert after successful login
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"免责声明",
|
"免责声明",
|
||||||
"本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。",
|
"本应用仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。",
|
||||||
[{ text: "确定" }]
|
[{ text: "确定" }]
|
||||||
);
|
);
|
||||||
} catch {
|
} catch (error) {
|
||||||
Toast.show({ type: "error", text1: "登录失败", text2: "用户名或密码错误" });
|
Toast.show({
|
||||||
|
type: "error",
|
||||||
|
text1: "登录失败",
|
||||||
|
text2: error instanceof Error ? error.message : "用户名或密码错误",
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useCallback, useRef, forwardRef } from "react";
|
import React, { useState, useEffect, useCallback, useRef, forwardRef } from "react";
|
||||||
import { View, Text, Image, StyleSheet, TouchableOpacity, Alert } from "react-native";
|
import { View, Text, Image, StyleSheet, TouchableOpacity, Alert, Animated } from "react-native";
|
||||||
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
|
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { Star, Play } from "lucide-react-native";
|
import { Star, Play } from "lucide-react-native";
|
||||||
import { PlayRecordManager } from "@/services/storage";
|
import { PlayRecordManager } from "@/services/storage";
|
||||||
@@ -46,16 +45,15 @@ const VideoCard = forwardRef<View, VideoCardProps>(
|
|||||||
) => {
|
) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const [fadeAnim] = useState(new Animated.Value(0));
|
||||||
|
|
||||||
const longPressTriggered = useRef(false);
|
const longPressTriggered = useRef(false);
|
||||||
|
|
||||||
const scale = useSharedValue(1);
|
const scale = useRef(new Animated.Value(1)).current;
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => {
|
const animatedStyle = {
|
||||||
return {
|
transform: [{ scale }],
|
||||||
transform: [{ scale: scale.value }],
|
};
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const handlePress = () => {
|
const handlePress = () => {
|
||||||
if (longPressTriggered.current) {
|
if (longPressTriggered.current) {
|
||||||
@@ -78,15 +76,32 @@ const VideoCard = forwardRef<View, VideoCardProps>(
|
|||||||
|
|
||||||
const handleFocus = useCallback(() => {
|
const handleFocus = useCallback(() => {
|
||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
scale.value = withSpring(1.05, { damping: 15, stiffness: 200 });
|
Animated.spring(scale, {
|
||||||
|
toValue: 1.05,
|
||||||
|
damping: 15,
|
||||||
|
stiffness: 200,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start();
|
||||||
onFocus?.();
|
onFocus?.();
|
||||||
}, [scale, onFocus]);
|
}, [scale, onFocus]);
|
||||||
|
|
||||||
const handleBlur = useCallback(() => {
|
const handleBlur = useCallback(() => {
|
||||||
setIsFocused(false);
|
setIsFocused(false);
|
||||||
scale.value = withSpring(1.0);
|
Animated.spring(scale, {
|
||||||
|
toValue: 1.0,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start();
|
||||||
}, [scale]);
|
}, [scale]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 400,
|
||||||
|
delay: Math.random() * 200, // 随机延迟创造交错效果
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start();
|
||||||
|
}, [fadeAnim]);
|
||||||
|
|
||||||
const handleLongPress = () => {
|
const handleLongPress = () => {
|
||||||
// Only allow long press for items with progress (play records)
|
// Only allow long press for items with progress (play records)
|
||||||
if (progress === undefined) return;
|
if (progress === undefined) return;
|
||||||
@@ -128,7 +143,7 @@ const VideoCard = forwardRef<View, VideoCardProps>(
|
|||||||
const isContinueWatching = progress !== undefined && progress > 0 && progress < 1;
|
const isContinueWatching = progress !== undefined && progress > 0 && progress < 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.wrapper, animatedStyle]}>
|
<Animated.View style={[styles.wrapper, animatedStyle, { opacity: fadeAnim }]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
onLongPress={handleLongPress}
|
onLongPress={handleLongPress}
|
||||||
@@ -260,9 +275,9 @@ const styles = StyleSheet.create({
|
|||||||
infoContainer: {
|
infoContainer: {
|
||||||
width: CARD_WIDTH,
|
width: CARD_WIDTH,
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
alignItems: "flex-start", // Align items to the start
|
alignItems: "flex-start",
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
paddingHorizontal: 4, // Add some padding
|
paddingHorizontal: 4,
|
||||||
},
|
},
|
||||||
infoRow: {
|
infoRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -328,4 +343,4 @@ const styles = StyleSheet.create({
|
|||||||
color: Colors.dark.primary,
|
color: Colors.dark.primary,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -70,6 +70,9 @@ interface HomeState {
|
|||||||
refreshPlayRecords: () => Promise<void>;
|
refreshPlayRecords: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 内存缓存,应用生命周期内有效
|
||||||
|
const dataCache = new Map<string, RowItem[]>();
|
||||||
|
|
||||||
const useHomeStore = create<HomeState>((set, get) => ({
|
const useHomeStore = create<HomeState>((set, get) => ({
|
||||||
categories: initialCategories,
|
categories: initialCategories,
|
||||||
selectedCategory: initialCategories[0],
|
selectedCategory: initialCategories[0],
|
||||||
@@ -83,6 +86,29 @@ const useHomeStore = create<HomeState>((set, get) => ({
|
|||||||
fetchInitialData: async () => {
|
fetchInitialData: async () => {
|
||||||
const { apiBaseUrl } = useSettingsStore.getState();
|
const { apiBaseUrl } = useSettingsStore.getState();
|
||||||
await useAuthStore.getState().checkLoginStatus(apiBaseUrl);
|
await useAuthStore.getState().checkLoginStatus(apiBaseUrl);
|
||||||
|
|
||||||
|
const { selectedCategory } = get();
|
||||||
|
const cacheKey = `${selectedCategory.title}-${selectedCategory.tag || ''}`;
|
||||||
|
|
||||||
|
// 最近播放不缓存,始终实时获取
|
||||||
|
if (selectedCategory.type === 'record') {
|
||||||
|
set({ loading: true, contentData: [], pageStart: 0, hasMore: true, error: null });
|
||||||
|
await get().loadMoreData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
if (dataCache.has(cacheKey)) {
|
||||||
|
set({
|
||||||
|
loading: false,
|
||||||
|
contentData: dataCache.get(cacheKey)!,
|
||||||
|
pageStart: dataCache.get(cacheKey)!.length,
|
||||||
|
hasMore: false,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
set({ loading: true, contentData: [], pageStart: 0, hasMore: true, error: null });
|
set({ loading: true, contentData: [], pageStart: 0, hasMore: true, error: null });
|
||||||
await get().loadMoreData();
|
await get().loadMoreData();
|
||||||
},
|
},
|
||||||
@@ -133,11 +159,25 @@ const useHomeStore = create<HomeState>((set, get) => ({
|
|||||||
id: item.title,
|
id: item.title,
|
||||||
source: "douban",
|
source: "douban",
|
||||||
})) as RowItem[];
|
})) as RowItem[];
|
||||||
set((state) => ({
|
|
||||||
contentData: pageStart === 0 ? newItems : [...state.contentData, ...newItems],
|
const cacheKey = `${selectedCategory.title}-${selectedCategory.tag || ''}`;
|
||||||
pageStart: state.pageStart + result.list.length,
|
|
||||||
hasMore: true,
|
if (pageStart === 0) {
|
||||||
}));
|
// 缓存新数据
|
||||||
|
dataCache.set(cacheKey, newItems);
|
||||||
|
set((state) => ({
|
||||||
|
contentData: newItems,
|
||||||
|
pageStart: result.list.length,
|
||||||
|
hasMore: true,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// 增量加载时不缓存,直接追加
|
||||||
|
set((state) => ({
|
||||||
|
contentData: [...state.contentData, ...newItems],
|
||||||
|
pageStart: state.pageStart + result.list.length,
|
||||||
|
hasMore: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (selectedCategory.tags) {
|
} else if (selectedCategory.tags) {
|
||||||
// It's a container category, do not load content, but clear current content
|
// It's a container category, do not load content, but clear current content
|
||||||
@@ -158,10 +198,29 @@ const useHomeStore = create<HomeState>((set, get) => ({
|
|||||||
|
|
||||||
selectCategory: (category: Category) => {
|
selectCategory: (category: Category) => {
|
||||||
const currentCategory = get().selectedCategory;
|
const currentCategory = get().selectedCategory;
|
||||||
// Only fetch new data if the category or tag actually changes
|
const cacheKey = `${category.title}-${category.tag || ''}`;
|
||||||
|
|
||||||
|
// 只有当分类或标签真正变化时才处理
|
||||||
if (currentCategory.title !== category.title || currentCategory.tag !== category.tag) {
|
if (currentCategory.title !== category.title || currentCategory.tag !== category.tag) {
|
||||||
set({ selectedCategory: category, contentData: [], pageStart: 0, hasMore: true, error: null });
|
set({ selectedCategory: category, contentData: [], pageStart: 0, hasMore: true, error: null });
|
||||||
get().fetchInitialData();
|
|
||||||
|
// 最近播放始终实时获取
|
||||||
|
if (category.type === 'record') {
|
||||||
|
get().fetchInitialData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查缓存,有则直接使用,无则请求
|
||||||
|
if (dataCache.has(cacheKey)) {
|
||||||
|
set({
|
||||||
|
contentData: dataCache.get(cacheKey)!,
|
||||||
|
pageStart: dataCache.get(cacheKey)!.length,
|
||||||
|
hasMore: false,
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
get().fetchInitialData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -199,6 +258,7 @@ const useHomeStore = create<HomeState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
||||||
get().fetchInitialData();
|
get().fetchInitialData();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user