Merge pull request #104 from zimplexing/v1.2.10

feat: Enhance UI with fade animations and implement data caching in h…
This commit is contained in:
Xin
2025-07-29 19:16:51 +08:00
committed by GitHub
4 changed files with 134 additions and 37 deletions

View File

@@ -1,5 +1,5 @@
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 { ThemedText } from "@/components/ThemedText";
import { api } from "@/services/api";
@@ -21,6 +21,7 @@ export default function HomeScreen() {
const router = useRouter();
const colorScheme = "dark";
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const fadeAnim = useRef(new Animated.Value(0)).current;
const {
categories,
@@ -59,6 +60,18 @@ export default function HomeScreen() {
}
}, [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) => {
setSelectedTag(null);
selectCategory(category);
@@ -196,18 +209,20 @@ export default function HomeScreen() {
</ThemedText>
</View>
) : (
<CustomScrollView
data={contentData}
renderItem={renderContentItem}
numColumns={NUM_COLUMNS}
loading={loading}
loadingMore={loadingMore}
error={error}
onEndReached={loadMoreData}
loadMoreThreshold={LOAD_MORE_THRESHOLD}
emptyMessage={selectedCategory?.tags ? "请选择一个子分类" : "该分类下暂无内容"}
ListFooterComponent={renderFooter}
/>
<Animated.View style={[styles.contentContainer, { opacity: fadeAnim }]}>
<CustomScrollView
data={contentData}
renderItem={renderContentItem}
numColumns={NUM_COLUMNS}
loading={loading}
loadingMore={loadingMore}
error={error}
onEndReached={loadMoreData}
loadMoreThreshold={LOAD_MORE_THRESHOLD}
emptyMessage={selectedCategory?.tags ? "请选择一个子分类" : "该分类下暂无内容"}
ListFooterComponent={renderFooter}
/>
</Animated.View>
)}
</ThemedView>
);
@@ -266,6 +281,9 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
paddingBottom: 20,
},
contentContainer: {
flex: 1,
},
itemContainer: {
margin: 8,
alignItems: "center",

View File

@@ -62,8 +62,12 @@ const LoginModal = () => {
"本应用仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。",
[{ text: "确定" }]
);
} catch {
Toast.show({ type: "error", text1: "登录失败", text2: "用户名或密码错误" });
} catch (error) {
Toast.show({
type: "error",
text1: "登录失败",
text2: error instanceof Error ? error.message : "用户名或密码错误",
});
} finally {
setIsLoading(false);
}

View File

@@ -1,6 +1,5 @@
import React, { useState, useCallback, useRef, forwardRef } from "react";
import { View, Text, Image, StyleSheet, TouchableOpacity, Alert } from "react-native";
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
import React, { useState, useEffect, useCallback, useRef, forwardRef } from "react";
import { View, Text, Image, StyleSheet, TouchableOpacity, Alert, Animated } from "react-native";
import { useRouter } from "expo-router";
import { Star, Play } from "lucide-react-native";
import { PlayRecordManager } from "@/services/storage";
@@ -46,16 +45,15 @@ const VideoCard = forwardRef<View, VideoCardProps>(
) => {
const router = useRouter();
const [isFocused, setIsFocused] = useState(false);
const [fadeAnim] = useState(new Animated.Value(0));
const longPressTriggered = useRef(false);
const scale = useSharedValue(1);
const scale = useRef(new Animated.Value(1)).current;
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
};
});
const animatedStyle = {
transform: [{ scale }],
};
const handlePress = () => {
if (longPressTriggered.current) {
@@ -78,15 +76,32 @@ const VideoCard = forwardRef<View, VideoCardProps>(
const handleFocus = useCallback(() => {
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?.();
}, [scale, onFocus]);
const handleBlur = useCallback(() => {
setIsFocused(false);
scale.value = withSpring(1.0);
Animated.spring(scale, {
toValue: 1.0,
useNativeDriver: true,
}).start();
}, [scale]);
useEffect(() => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 400,
delay: Math.random() * 200, // 随机延迟创造交错效果
useNativeDriver: true,
}).start();
}, [fadeAnim]);
const handleLongPress = () => {
// Only allow long press for items with progress (play records)
if (progress === undefined) return;
@@ -128,7 +143,7 @@ const VideoCard = forwardRef<View, VideoCardProps>(
const isContinueWatching = progress !== undefined && progress > 0 && progress < 1;
return (
<Animated.View style={[styles.wrapper, animatedStyle]}>
<Animated.View style={[styles.wrapper, animatedStyle, { opacity: fadeAnim }]}>
<TouchableOpacity
onPress={handlePress}
onLongPress={handleLongPress}
@@ -260,9 +275,9 @@ const styles = StyleSheet.create({
infoContainer: {
width: CARD_WIDTH,
marginTop: 8,
alignItems: "flex-start", // Align items to the start
alignItems: "flex-start",
marginBottom: 16,
paddingHorizontal: 4, // Add some padding
paddingHorizontal: 4,
},
infoRow: {
flexDirection: "row",
@@ -328,4 +343,4 @@ const styles = StyleSheet.create({
color: Colors.dark.primary,
fontSize: 12,
},
});
});

View File

@@ -70,6 +70,9 @@ interface HomeState {
refreshPlayRecords: () => Promise<void>;
}
// 内存缓存,应用生命周期内有效
const dataCache = new Map<string, RowItem[]>();
const useHomeStore = create<HomeState>((set, get) => ({
categories: initialCategories,
selectedCategory: initialCategories[0],
@@ -83,6 +86,29 @@ const useHomeStore = create<HomeState>((set, get) => ({
fetchInitialData: async () => {
const { apiBaseUrl } = useSettingsStore.getState();
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 });
await get().loadMoreData();
},
@@ -133,11 +159,25 @@ const useHomeStore = create<HomeState>((set, get) => ({
id: item.title,
source: "douban",
})) as RowItem[];
set((state) => ({
contentData: pageStart === 0 ? newItems : [...state.contentData, ...newItems],
pageStart: state.pageStart + result.list.length,
hasMore: true,
}));
const cacheKey = `${selectedCategory.title}-${selectedCategory.tag || ''}`;
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) {
// 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) => {
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) {
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 {};
});
get().fetchInitialData();
},
}));