From dcdab410c65b87a756b8527edfee95a19f11a1b2 Mon Sep 17 00:00:00 2001 From: imeepos Date: Fri, 16 Jan 2026 15:16:49 +0800 Subject: [PATCH] fix: bug --- .env | 0 app/(tabs)/index.tsx | 81 ++++++++++++++---- app/(tabs)/video.tsx | 66 ++++++--------- app/channels.tsx | 160 ++++++++++++++++++++++++------------ bun.lock | 8 +- hooks/index.ts | 2 + hooks/use-search-history.ts | 74 +++++++++++++++++ hooks/use-templates.ts | 7 +- locales/en-US.json | 7 +- locales/zh-CN.json | 7 +- package.json | 4 +- 11 files changed, 292 insertions(+), 124 deletions(-) create mode 100644 .env create mode 100644 hooks/use-search-history.ts diff --git a/.env b/.env new file mode 100644 index 0000000..e69de29 diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index c8afa5c..e80462f 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,6 +1,7 @@ import { Image } from 'expo-image' +import { VideoView, useVideoPlayer } from 'expo-video' import { LinearGradient } from 'expo-linear-gradient' -import { useRouter } from 'expo-router' +import { useRouter, useLocalSearchParams } from 'expo-router' import { StatusBar } from 'expo-status-bar' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -23,10 +24,29 @@ import { useCategories } from '@/hooks/use-categories' const { width: screenWidth } = Dimensions.get('window') +// 视频预览组件 +function VideoPreview({ source, style }: { source: string; style: any }) { + const player = useVideoPlayer(source, (player) => { + player.loop = true + player.muted = true + player.play() + }) + + return ( + + ) +} + export default function HomeScreen() { const { t } = useTranslation() const insets = useSafeAreaInsets() const router = useRouter() + const params = useLocalSearchParams() const [activeTab, setActiveTab] = useState(0) const [selectedCategoryId, setSelectedCategoryId] = useState(null) @@ -50,6 +70,19 @@ export default function HomeScreen() { } }, [categoriesData, categoriesError]) + // 监听从 channels 页面传递过来的 categoryId 参数 + useEffect(() => { + const categoryIdFromParams = params.categoryId as string | undefined + if (categoryIdFromParams && categoriesData?.categories) { + setSelectedCategoryId(categoryIdFromParams) + // 同时更新 activeTab 索引 + const categoryIndex = categoriesData.categories.findIndex(cat => cat.id === categoryIdFromParams) + if (categoryIndex !== -1) { + setActiveTab(categoryIndex) + } + } + }, [params.categoryId, categoriesData]) + // 使用接口返回的分类数据,如果没有则使用默认翻译 const categories = categoriesData?.categories || [] const tabs = categories.length > 0 @@ -69,17 +102,22 @@ export default function HomeScreen() { // 将 CategoryTemplate 数据转换为卡片数据格式 const displayCardData = categoryTemplates.length > 0 - ? categoryTemplates.map((template, idx) => ({ - id: template.id || `template-${idx}`, - title: template.title, - image: { uri: template.previewUrl || template.coverImageUrl || `` }, - isHot: false, - users: 0, - height: undefined, - previewUrl: template.previewUrl, - aspectRatio: template.aspectRatio, - tag: template.tag, - })) + ? categoryTemplates.map((template, idx) => { + const previewUrl = (template as any).webpPreviewUrl || template.previewUrl || template.coverImageUrl || `` + const isVideo = previewUrl.includes('.mp4') || previewUrl.includes('.mov') || previewUrl.includes('.webm') + return { + id: template.id || `template-${idx}`, + title: template.title, + image: { uri: previewUrl }, + isHot: false, + users: 0, + height: undefined, + previewUrl: template.previewUrl, + aspectRatio: template.aspectRatio, + tag: template.tag, + isVideo, + } + }) : [] const [gridWidth, setGridWidth] = useState(screenWidth) const [showTabArrow, setShowTabArrow] = useState(false) @@ -164,7 +202,10 @@ export default function HomeScreen() { > router.push('/channels')} + onPress={() => router.push({ + pathname: '/channels' as any, + params: selectedCategoryId ? { categoryId: selectedCategoryId } : undefined, + })} > @@ -321,11 +362,15 @@ export default function HomeScreen() { { height: card.height || cardWidth * 1.2 }, ]} > - + {card.isVideo ? ( + + ) : ( + + )} { const { t } = useTranslation() const router = useRouter() - const [isPlaying, setIsPlaying] = useState(false) const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null) const handleImageLoad = useCallback((event: ImageLoadEventData) => { @@ -91,21 +91,19 @@ const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeig const imageStyle = calculateImageSize(imageSize, videoHeight) + // 优先使用 WebP 格式(支持动画),回退到普通预览图 + const displayUrl = item.webpPreviewUrl || item.previewUrl + return ( - - {!isPlaying && ( - setIsPlaying(true)}> - - - - + {displayUrl && ( + )} @@ -126,11 +124,12 @@ const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeig export default function VideoScreen() { const flatListRef = useRef(null) const videoHeight = screenHeight - TAB_BAR_HEIGHT + const PAGE_SIZE = 10 // 每页10个数据 const { templates, loading, error, execute, refetch, loadMore, hasMore } = useTemplates({ sortBy: 'likeCount', sortOrder: 'desc', page: 1, - limit: 20, + limit: PAGE_SIZE, }) const [refreshing, setRefreshing] = useState(false) @@ -145,9 +144,14 @@ export default function VideoScreen() { }, [refetch]) const handleLoadMore = useCallback(() => { - if (!hasMore || loading) return + console.log('onEndReached triggered', { hasMore, loading, templatesLength: templates.length }) + if (!hasMore || loading) { + console.log('LoadMore blocked', { hasMore, loading }) + return + } + console.log('Loading more...') loadMore() - }, [hasMore, loading, loadMore]) + }, [hasMore, loading, loadMore, templates.length]) const renderItem = useCallback(({ item }: { item: TemplateDetail }) => ( @@ -186,8 +190,11 @@ export default function VideoScreen() { /> } onEndReached={handleLoadMore} - onEndReachedThreshold={0.5} + onEndReachedThreshold={0.1} ListFooterComponent={loading ? : null} + maxToRenderPerBatch={5} + windowSize={7} + initialNumToRender={3} /> ) @@ -223,31 +230,6 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - playButton: { - ...StyleSheet.absoluteFillObject, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.3)', - }, - playIcon: { - width: 56, - height: 56, - borderRadius: 28, - backgroundColor: 'rgba(255, 255, 255, 0.85)', - alignItems: 'center', - justifyContent: 'center', - }, - playTriangle: { - width: 0, - height: 0, - borderLeftWidth: 18, - borderTopWidth: 11, - borderBottomWidth: 11, - borderLeftColor: '#000000', - borderTopColor: 'transparent', - borderBottomColor: 'transparent', - marginLeft: 3, - }, thumbnailContainer: { position: 'absolute', left: 12, diff --git a/app/channels.tsx b/app/channels.tsx index 00b7510..5e11668 100644 --- a/app/channels.tsx +++ b/app/channels.tsx @@ -1,32 +1,46 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { View, Text, StyleSheet, Pressable, StatusBar as RNStatusBar, + ActivityIndicator, } from 'react-native' import { StatusBar } from 'expo-status-bar' import { LinearGradient } from 'expo-linear-gradient' import { SafeAreaView } from 'react-native-safe-area-context' -import { useRouter } from 'expo-router' +import { useRouter, useLocalSearchParams } from 'expo-router' import { useTranslation } from 'react-i18next' import { TopArrowIcon } from '@/components/icon' import GradientText from '@/components/GradientText' +import { useCategories } from '@/hooks/use-categories' export default function ChannelsScreen() { const { t } = useTranslation() const router = useRouter() + const params = useLocalSearchParams() const [isExpanded, setIsExpanded] = useState(true) - const [selectedChannel, setSelectedChannel] = useState(2) // 默认选中第二个频道 - - // 频道数据 - const channels = Array.from({ length: 13 }, (_, i) => ({ - id: i + 1, - name: t('channels.channelName'), - })) + + // 使用 useCategories hook 获取分类数据 + const { load, data: categoriesData, loading: categoriesLoading, error: categoriesError } = useCategories() + + // 从路由参数获取当前选中的分类 ID + const currentCategoryId = params.categoryId as string | undefined + + useEffect(() => { + // 加载分类数据 + load() + }, []) + + // 使用接口返回的分类数据 + const categories = categoriesData?.categories || [] + + // 如果没有分类数据,显示加载状态或空状态 + const showLoading = categoriesLoading + const showEmptyState = !categoriesLoading && categories.length === 0 return ( @@ -41,11 +55,7 @@ export default function ChannelsScreen() { {t('channels.title')} { - if (isExpanded) { - router.push('/') - } else { - setIsExpanded(true) - } + router.back() }} > @@ -55,45 +65,65 @@ export default function ChannelsScreen() { {/* 频道选择区域 */} {isExpanded && ( - - {channels.map((channel) => { - return ( - setSelectedChannel(channel.id)} - style={[styles.channelButtonWrapper]} - > - {selectedChannel === channel.id ? ( - - - - {channel.name} - + {showLoading ? ( + + + + ) : showEmptyState ? ( + + {t('channels.noCategories') || '暂无分类'} + load()}> + {t('channels.retry') || '重新加载'} + + + ) : ( + + {categories.map((category) => { + const isSelected = currentCategoryId === category.id + return ( + { + // 选择分类后返回首页 + router.push({ + pathname: '/' as any, + params: { categoryId: category.id }, + }) + }} + style={[styles.channelButtonWrapper]} + > + {isSelected ? ( + + + + {category.name} + + + + ) : ( + + + + {category.name} + + - - ) : ( - - - - {channel.name} - - - - )} - - ) - })} - + )} + + ) + })} + + )} )} @@ -169,5 +199,31 @@ const styles = StyleSheet.create({ fontWeight: '500', textAlign: 'center', }, + loadingContainer: { + paddingVertical: 24, + alignItems: 'center', + justifyContent: 'center', + }, + emptyStateContainer: { + paddingVertical: 40, + alignItems: 'center', + justifyContent: 'center', + gap: 16, + }, + emptyStateText: { + color: '#ABABAB', + fontSize: 14, + }, + retryButton: { + backgroundColor: '#FF6699', + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 8, + }, + retryButtonText: { + color: '#FFFFFF', + fontSize: 14, + fontWeight: '600', + }, }) diff --git a/bun.lock b/bun.lock index 39a0bca..97dad8c 100644 --- a/bun.lock +++ b/bun.lock @@ -13,8 +13,8 @@ "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", - "@repo/core": "1.0.1", - "@repo/sdk": "1.0.4", + "@repo/core": "1.0.2", + "@repo/sdk": "1.0.7", "@stripe/react-stripe-js": "^5.4.1", "@stripe/stripe-js": "^8.5.3", "@stripe/stripe-react-native": "^0.57.0", @@ -625,9 +625,9 @@ "@react-navigation/routers": ["@react-navigation/routers@7.5.2", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-kymreY5aeTz843E+iPAukrsOtc7nabAH6novtAPREmmGu77dQpfxPB2ZWpKb5nRErIRowp1kYRoN2Ckl+S6JYw=="], - "@repo/core": ["@repo/core@1.0.1", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fcore/-/1.0.1/core-1.0.1.tgz", {}, "sha512-dzdae2NBT0L4GWCtz6PscmaRvElGFXWeJ46vQhDYc2z49wjnRYRxZgIcwB5bxXjfYZF3sj0cnbbs5mz8F16oAw=="], + "@repo/core": ["@repo/core@1.0.2", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fcore/-/1.0.2/core-1.0.2.tgz", {}, "sha512-/d1L9I+9u8rHiWBNXW2GC5hB6okd/EoePpcuiJrbbtRH8rZuQOVtdRDuDHIokrL0eFN9rEi3zaoFdvyZgD0hrg=="], - "@repo/sdk": ["@repo/sdk@1.0.4", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fsdk/-/1.0.4/sdk-1.0.4.tgz", { "dependencies": { "@repo/core": "1.0.1", "reflect-metadata": "^0.2.1", "zod": "^4.2.1" } }, "sha512-uMjxK4G2aEvssshtcxBIZsDSnP2JYLC8ClB/GrstjKeBuElbZKTAomaeIL8PmZILfIA96Jq6sWKh5ZlKVK2jfg=="], + "@repo/sdk": ["@repo/sdk@1.0.7", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fsdk/-/1.0.7/sdk-1.0.7.tgz", { "dependencies": { "@repo/core": "1.0.2", "reflect-metadata": "^0.2.1", "zod": "^4.2.1" } }, "sha512-KQ8bgj3fA85xFN3X6rVkM19wk5NhXoc3un5jEUn05xBXGmWtDLZ1TgbF1vR1fSz8XBejnxnlb6c9AtqjDthFPw=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], diff --git a/hooks/index.ts b/hooks/index.ts index 116628f..cae00b8 100644 --- a/hooks/index.ts +++ b/hooks/index.ts @@ -4,3 +4,5 @@ export { useTemplateActions } from './use-template-actions' export { useTemplates, type TemplateDetail } from './use-templates' export { useTemplateDetail, type TemplateDetail as TemplateDetailType } from './use-template-detail' export { useTemplateGenerations, type TemplateGeneration } from './use-template-generations' +export { useSearchHistory } from './use-search-history' +export { useTags } from './use-tags' diff --git a/hooks/use-search-history.ts b/hooks/use-search-history.ts new file mode 100644 index 0000000..ee2832f --- /dev/null +++ b/hooks/use-search-history.ts @@ -0,0 +1,74 @@ +import { useCallback, useEffect, useState } from 'react' +import AsyncStorage from '@react-native-async-storage/async-storage' + +const STORAGE_KEY = '@searchHistory' +const MAX_HISTORY_LENGTH = 10 + +interface UseSearchHistoryReturn { + history: string[] + addToHistory: (keyword: string) => Promise + removeFromHistory: (keyword: string) => Promise + clearHistory: () => Promise + isLoading: boolean +} + +export function useSearchHistory(): UseSearchHistoryReturn { + const [history, setHistory] = useState([]) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + loadHistory() + }, []) + + const loadHistory = useCallback(async () => { + try { + const stored = await AsyncStorage.getItem(STORAGE_KEY) + setHistory(stored ? JSON.parse(stored) : []) + } catch { + setHistory([]) + } finally { + setIsLoading(false) + } + }, []) + + const saveHistory = useCallback(async (newHistory: string[]) => { + try { + await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newHistory)) + setHistory(newHistory) + } catch { + setHistory(newHistory) + } + }, []) + + const addToHistory = useCallback( + async (keyword: string) => { + const trimmed = keyword.trim() + if (!trimmed) return + + const filtered = history.filter((item) => item !== trimmed) + const updated = [trimmed, ...filtered].slice(0, MAX_HISTORY_LENGTH) + await saveHistory(updated) + }, + [history, saveHistory] + ) + + const removeFromHistory = useCallback( + async (keyword: string) => { + const updated = history.filter((item) => item !== keyword) + await saveHistory(updated) + }, + [history, saveHistory] + ) + + const clearHistory = useCallback(async () => { + await saveHistory([]) + }, [saveHistory]) + + return { + history, + addToHistory, + removeFromHistory, + clearHistory, + isLoading, + } +} diff --git a/hooks/use-templates.ts b/hooks/use-templates.ts index a444b27..761aa71 100644 --- a/hooks/use-templates.ts +++ b/hooks/use-templates.ts @@ -49,7 +49,9 @@ export const useTemplates = (initialParams?: ListTemplatesParams) => { } const templates = data?.templates || [] - hasMoreRef.current = templates.length >= (params?.limit || DEFAULT_PARAMS.limit) + const currentPage = requestParams.page || 1 + const totalPages = data?.totalPages || 1 + hasMoreRef.current = currentPage < totalPages setData(data) setLoading(false) return { data, error: null } @@ -79,7 +81,8 @@ export const useTemplates = (initialParams?: ListTemplatesParams) => { } const newTemplates = newData?.templates || [] - hasMoreRef.current = newTemplates.length >= DEFAULT_PARAMS.limit + const totalPages = newData?.totalPages || 1 + hasMoreRef.current = nextPage < totalPages currentPageRef.current = nextPage setData((prev) => ({ diff --git a/locales/en-US.json b/locales/en-US.json index 89933d0..ffd7f66 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -93,7 +93,9 @@ }, "channels": { "title": "All Channels", - "channelName": "Channel Name" + "channelName": "Channel Name", + "noCategories": "No categories available", + "retry": "Retry" }, "notFound": { "title": "Page Not Found", @@ -120,7 +122,8 @@ "exploreMore": "Explore More", "refresh": "Refresh", "searchWorks": "Search Generated Works", - "searchWorksPlaceholder": "Enter keywords to search generated works" + "searchWorksPlaceholder": "Enter keywords to search generated works", + "noTags": "No recommended tags" }, "templateDetail": { "title": "Hello, I'm a new resident of Zootopia 👋", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 7071f0e..fbd0b57 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -93,7 +93,9 @@ }, "channels": { "title": "全部频道", - "channelName": "频道名称" + "channelName": "频道名称", + "noCategories": "暂无分类", + "retry": "重新加载" }, "notFound": { "title": "页面未找到", @@ -120,7 +122,8 @@ "exploreMore": "探索更多", "refresh": "换一换", "searchWorks": "搜索生成的作品", - "searchWorksPlaceholder": "请输入关键词搜索生成的作品" + "searchWorksPlaceholder": "请输入关键词搜索生成的作品", + "noTags": "暂无推荐标签" }, "templateDetail": { "title": "泥嚎 我是动物城的新居民 👋", diff --git a/package.json b/package.json index 9f1ff08..cf8d4b2 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "lint": "expo lint" }, "dependencies": { - "@repo/core": "1.0.1", - "@repo/sdk": "1.0.4", + "@repo/core": "1.0.2", + "@repo/sdk": "1.0.7", "@better-auth/expo": "1.3.34", "@expo/vector-icons": "^15.0.3", "@gorhom/bottom-sheet": "^5.2.8",