fix: bug
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { Image } from 'expo-image'
|
import { Image } from 'expo-image'
|
||||||
|
import { VideoView, useVideoPlayer } from 'expo-video'
|
||||||
import { LinearGradient } from 'expo-linear-gradient'
|
import { LinearGradient } from 'expo-linear-gradient'
|
||||||
import { useRouter } from 'expo-router'
|
import { useRouter, useLocalSearchParams } from 'expo-router'
|
||||||
import { StatusBar } from 'expo-status-bar'
|
import { StatusBar } from 'expo-status-bar'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -23,10 +24,29 @@ import { useCategories } from '@/hooks/use-categories'
|
|||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get('window')
|
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 (
|
||||||
|
<VideoView
|
||||||
|
style={style}
|
||||||
|
player={player}
|
||||||
|
allowsFullscreen
|
||||||
|
allowsPictureInPicture
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const params = useLocalSearchParams()
|
||||||
const [activeTab, setActiveTab] = useState(0)
|
const [activeTab, setActiveTab] = useState(0)
|
||||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null)
|
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null)
|
||||||
|
|
||||||
@@ -50,6 +70,19 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
}, [categoriesData, categoriesError])
|
}, [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 categories = categoriesData?.categories || []
|
||||||
const tabs = categories.length > 0
|
const tabs = categories.length > 0
|
||||||
@@ -69,17 +102,22 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
// 将 CategoryTemplate 数据转换为卡片数据格式
|
// 将 CategoryTemplate 数据转换为卡片数据格式
|
||||||
const displayCardData = categoryTemplates.length > 0
|
const displayCardData = categoryTemplates.length > 0
|
||||||
? categoryTemplates.map((template, idx) => ({
|
? 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}`,
|
id: template.id || `template-${idx}`,
|
||||||
title: template.title,
|
title: template.title,
|
||||||
image: { uri: template.previewUrl || template.coverImageUrl || `` },
|
image: { uri: previewUrl },
|
||||||
isHot: false,
|
isHot: false,
|
||||||
users: 0,
|
users: 0,
|
||||||
height: undefined,
|
height: undefined,
|
||||||
previewUrl: template.previewUrl,
|
previewUrl: template.previewUrl,
|
||||||
aspectRatio: template.aspectRatio,
|
aspectRatio: template.aspectRatio,
|
||||||
tag: template.tag,
|
tag: template.tag,
|
||||||
}))
|
isVideo,
|
||||||
|
}
|
||||||
|
})
|
||||||
: []
|
: []
|
||||||
const [gridWidth, setGridWidth] = useState(screenWidth)
|
const [gridWidth, setGridWidth] = useState(screenWidth)
|
||||||
const [showTabArrow, setShowTabArrow] = useState(false)
|
const [showTabArrow, setShowTabArrow] = useState(false)
|
||||||
@@ -164,7 +202,10 @@ export default function HomeScreen() {
|
|||||||
>
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.tabArrow}
|
style={styles.tabArrow}
|
||||||
onPress={() => router.push('/channels')}
|
onPress={() => router.push({
|
||||||
|
pathname: '/channels' as any,
|
||||||
|
params: selectedCategoryId ? { categoryId: selectedCategoryId } : undefined,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<DownArrowIcon />
|
<DownArrowIcon />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -321,11 +362,15 @@ export default function HomeScreen() {
|
|||||||
{ height: card.height || cardWidth * 1.2 },
|
{ height: card.height || cardWidth * 1.2 },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
{card.isVideo ? (
|
||||||
|
<VideoPreview source={card.image.uri} style={styles.cardImage} />
|
||||||
|
) : (
|
||||||
<Image
|
<Image
|
||||||
source={card.image}
|
source={card.image}
|
||||||
style={styles.cardImage}
|
style={styles.cardImage}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['rgba(17, 17, 17, 0)', 'rgba(17, 17, 17, 0.9)']}
|
colors={['rgba(17, 17, 17, 0)', 'rgba(17, 17, 17, 0.9)']}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
StatusBar as RNStatusBar,
|
StatusBar as RNStatusBar,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
|
Platform,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { StatusBar } from 'expo-status-bar'
|
import { StatusBar } from 'expo-status-bar'
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||||
@@ -72,7 +73,6 @@ const calculateImageSize = (
|
|||||||
const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeight: number }) => {
|
const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeight: number }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [isPlaying, setIsPlaying] = useState(false)
|
|
||||||
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null)
|
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null)
|
||||||
|
|
||||||
const handleImageLoad = useCallback((event: ImageLoadEventData) => {
|
const handleImageLoad = useCallback((event: ImageLoadEventData) => {
|
||||||
@@ -91,21 +91,19 @@ const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeig
|
|||||||
|
|
||||||
const imageStyle = calculateImageSize(imageSize, videoHeight)
|
const imageStyle = calculateImageSize(imageSize, videoHeight)
|
||||||
|
|
||||||
|
// 优先使用 WebP 格式(支持动画),回退到普通预览图
|
||||||
|
const displayUrl = item.webpPreviewUrl || item.previewUrl
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.videoContainer, { height: videoHeight }]}>
|
<View style={[styles.videoContainer, { height: videoHeight }]}>
|
||||||
<View style={styles.videoWrapper}>
|
<View style={styles.videoWrapper}>
|
||||||
|
{displayUrl && (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: item.previewUrl }}
|
source={{ uri: displayUrl }}
|
||||||
style={imageStyle}
|
style={imageStyle}
|
||||||
contentFit="contain"
|
contentFit="contain"
|
||||||
onLoad={handleImageLoad}
|
onLoad={handleImageLoad}
|
||||||
/>
|
/>
|
||||||
{!isPlaying && (
|
|
||||||
<Pressable style={styles.playButton} onPress={() => setIsPlaying(true)}>
|
|
||||||
<View style={styles.playIcon}>
|
|
||||||
<View style={styles.playTriangle} />
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
)}
|
)}
|
||||||
<View style={styles.thumbnailContainer}>
|
<View style={styles.thumbnailContainer}>
|
||||||
<Image source={{ uri: item.coverImageUrl }} style={styles.thumbnail} contentFit="cover" />
|
<Image source={{ uri: item.coverImageUrl }} style={styles.thumbnail} contentFit="cover" />
|
||||||
@@ -126,11 +124,12 @@ const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeig
|
|||||||
export default function VideoScreen() {
|
export default function VideoScreen() {
|
||||||
const flatListRef = useRef<FlatList>(null)
|
const flatListRef = useRef<FlatList>(null)
|
||||||
const videoHeight = screenHeight - TAB_BAR_HEIGHT
|
const videoHeight = screenHeight - TAB_BAR_HEIGHT
|
||||||
|
const PAGE_SIZE = 10 // 每页10个数据
|
||||||
const { templates, loading, error, execute, refetch, loadMore, hasMore } = useTemplates({
|
const { templates, loading, error, execute, refetch, loadMore, hasMore } = useTemplates({
|
||||||
sortBy: 'likeCount',
|
sortBy: 'likeCount',
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 20,
|
limit: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
@@ -145,9 +144,14 @@ export default function VideoScreen() {
|
|||||||
}, [refetch])
|
}, [refetch])
|
||||||
|
|
||||||
const handleLoadMore = useCallback(() => {
|
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()
|
loadMore()
|
||||||
}, [hasMore, loading, loadMore])
|
}, [hasMore, loading, loadMore, templates.length])
|
||||||
|
|
||||||
const renderItem = useCallback(({ item }: { item: TemplateDetail }) => (
|
const renderItem = useCallback(({ item }: { item: TemplateDetail }) => (
|
||||||
<VideoItem item={item} videoHeight={videoHeight} />
|
<VideoItem item={item} videoHeight={videoHeight} />
|
||||||
@@ -186,8 +190,11 @@ export default function VideoScreen() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onEndReached={handleLoadMore}
|
onEndReached={handleLoadMore}
|
||||||
onEndReachedThreshold={0.5}
|
onEndReachedThreshold={0.1}
|
||||||
ListFooterComponent={loading ? <FooterLoading /> : null}
|
ListFooterComponent={loading ? <FooterLoading /> : null}
|
||||||
|
maxToRenderPerBatch={5}
|
||||||
|
windowSize={7}
|
||||||
|
initialNumToRender={3}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
@@ -223,31 +230,6 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: '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: {
|
thumbnailContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 12,
|
left: 12,
|
||||||
|
|||||||
@@ -1,32 +1,46 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Pressable,
|
Pressable,
|
||||||
StatusBar as RNStatusBar,
|
StatusBar as RNStatusBar,
|
||||||
|
ActivityIndicator,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { StatusBar } from 'expo-status-bar'
|
import { StatusBar } from 'expo-status-bar'
|
||||||
import { LinearGradient } from 'expo-linear-gradient'
|
import { LinearGradient } from 'expo-linear-gradient'
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
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 { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { TopArrowIcon } from '@/components/icon'
|
import { TopArrowIcon } from '@/components/icon'
|
||||||
import GradientText from '@/components/GradientText'
|
import GradientText from '@/components/GradientText'
|
||||||
|
import { useCategories } from '@/hooks/use-categories'
|
||||||
|
|
||||||
|
|
||||||
export default function ChannelsScreen() {
|
export default function ChannelsScreen() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const params = useLocalSearchParams()
|
||||||
const [isExpanded, setIsExpanded] = useState(true)
|
const [isExpanded, setIsExpanded] = useState(true)
|
||||||
const [selectedChannel, setSelectedChannel] = useState(2) // 默认选中第二个频道
|
|
||||||
|
|
||||||
// 频道数据
|
// 使用 useCategories hook 获取分类数据
|
||||||
const channels = Array.from({ length: 13 }, (_, i) => ({
|
const { load, data: categoriesData, loading: categoriesLoading, error: categoriesError } = useCategories()
|
||||||
id: i + 1,
|
|
||||||
name: t('channels.channelName'),
|
// 从路由参数获取当前选中的分类 ID
|
||||||
}))
|
const currentCategoryId = params.categoryId as string | undefined
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 加载分类数据
|
||||||
|
load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 使用接口返回的分类数据
|
||||||
|
const categories = categoriesData?.categories || []
|
||||||
|
|
||||||
|
// 如果没有分类数据,显示加载状态或空状态
|
||||||
|
const showLoading = categoriesLoading
|
||||||
|
const showEmptyState = !categoriesLoading && categories.length === 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.screen}>
|
<View style={styles.screen}>
|
||||||
@@ -41,11 +55,7 @@ export default function ChannelsScreen() {
|
|||||||
<Text style={styles.headerTitle}>{t('channels.title')}</Text>
|
<Text style={styles.headerTitle}>{t('channels.title')}</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (isExpanded) {
|
router.back()
|
||||||
router.push('/')
|
|
||||||
} else {
|
|
||||||
setIsExpanded(true)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TopArrowIcon />
|
<TopArrowIcon />
|
||||||
@@ -55,15 +65,34 @@ export default function ChannelsScreen() {
|
|||||||
{/* 频道选择区域 */}
|
{/* 频道选择区域 */}
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<View style={styles.channelsSection}>
|
<View style={styles.channelsSection}>
|
||||||
|
{showLoading ? (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="small" color="#FF6699" />
|
||||||
|
</View>
|
||||||
|
) : showEmptyState ? (
|
||||||
|
<View style={styles.emptyStateContainer}>
|
||||||
|
<Text style={styles.emptyStateText}>{t('channels.noCategories') || '暂无分类'}</Text>
|
||||||
|
<Pressable style={styles.retryButton} onPress={() => load()}>
|
||||||
|
<Text style={styles.retryButtonText}>{t('channels.retry') || '重新加载'}</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
<View style={styles.channelsGrid}>
|
<View style={styles.channelsGrid}>
|
||||||
{channels.map((channel) => {
|
{categories.map((category) => {
|
||||||
|
const isSelected = currentCategoryId === category.id
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={channel.id}
|
key={category.id}
|
||||||
onPress={() => setSelectedChannel(channel.id)}
|
onPress={() => {
|
||||||
|
// 选择分类后返回首页
|
||||||
|
router.push({
|
||||||
|
pathname: '/' as any,
|
||||||
|
params: { categoryId: category.id },
|
||||||
|
})
|
||||||
|
}}
|
||||||
style={[styles.channelButtonWrapper]}
|
style={[styles.channelButtonWrapper]}
|
||||||
>
|
>
|
||||||
{selectedChannel === channel.id ? (
|
{isSelected ? (
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['#FF9966', '#FF6699', '#9966FF']}
|
colors={['#FF9966', '#FF6699', '#9966FF']}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
@@ -77,7 +106,7 @@ export default function ChannelsScreen() {
|
|||||||
end={{ x: 1, y: 0 }}
|
end={{ x: 1, y: 0 }}
|
||||||
style={styles.channelButtonText}
|
style={styles.channelButtonText}
|
||||||
>
|
>
|
||||||
{channel.name}
|
{category.name}
|
||||||
</GradientText>
|
</GradientText>
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
@@ -85,7 +114,7 @@ export default function ChannelsScreen() {
|
|||||||
<View style={styles.channelButtonWrapperUnselected}>
|
<View style={styles.channelButtonWrapperUnselected}>
|
||||||
<View style={styles.channelButtonDefault}>
|
<View style={styles.channelButtonDefault}>
|
||||||
<Text style={styles.channelButtonText}>
|
<Text style={styles.channelButtonText}>
|
||||||
{channel.name}
|
{category.name}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -94,6 +123,7 @@ export default function ChannelsScreen() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
@@ -169,5 +199,31 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
textAlign: 'center',
|
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',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
8
bun.lock
8
bun.lock
@@ -13,8 +13,8 @@
|
|||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
"@repo/core": "1.0.1",
|
"@repo/core": "1.0.2",
|
||||||
"@repo/sdk": "1.0.4",
|
"@repo/sdk": "1.0.7",
|
||||||
"@stripe/react-stripe-js": "^5.4.1",
|
"@stripe/react-stripe-js": "^5.4.1",
|
||||||
"@stripe/stripe-js": "^8.5.3",
|
"@stripe/stripe-js": "^8.5.3",
|
||||||
"@stripe/stripe-react-native": "^0.57.0",
|
"@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=="],
|
"@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=="],
|
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ export { useTemplateActions } from './use-template-actions'
|
|||||||
export { useTemplates, type TemplateDetail } from './use-templates'
|
export { useTemplates, type TemplateDetail } from './use-templates'
|
||||||
export { useTemplateDetail, type TemplateDetail as TemplateDetailType } from './use-template-detail'
|
export { useTemplateDetail, type TemplateDetail as TemplateDetailType } from './use-template-detail'
|
||||||
export { useTemplateGenerations, type TemplateGeneration } from './use-template-generations'
|
export { useTemplateGenerations, type TemplateGeneration } from './use-template-generations'
|
||||||
|
export { useSearchHistory } from './use-search-history'
|
||||||
|
export { useTags } from './use-tags'
|
||||||
|
|||||||
74
hooks/use-search-history.ts
Normal file
74
hooks/use-search-history.ts
Normal file
@@ -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<void>
|
||||||
|
removeFromHistory: (keyword: string) => Promise<void>
|
||||||
|
clearHistory: () => Promise<void>
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSearchHistory(): UseSearchHistoryReturn {
|
||||||
|
const [history, setHistory] = useState<string[]>([])
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,7 +49,9 @@ export const useTemplates = (initialParams?: ListTemplatesParams) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const templates = data?.templates || []
|
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)
|
setData(data)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return { data, error: null }
|
return { data, error: null }
|
||||||
@@ -79,7 +81,8 @@ export const useTemplates = (initialParams?: ListTemplatesParams) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newTemplates = newData?.templates || []
|
const newTemplates = newData?.templates || []
|
||||||
hasMoreRef.current = newTemplates.length >= DEFAULT_PARAMS.limit
|
const totalPages = newData?.totalPages || 1
|
||||||
|
hasMoreRef.current = nextPage < totalPages
|
||||||
currentPageRef.current = nextPage
|
currentPageRef.current = nextPage
|
||||||
|
|
||||||
setData((prev) => ({
|
setData((prev) => ({
|
||||||
|
|||||||
@@ -93,7 +93,9 @@
|
|||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
"title": "All Channels",
|
"title": "All Channels",
|
||||||
"channelName": "Channel Name"
|
"channelName": "Channel Name",
|
||||||
|
"noCategories": "No categories available",
|
||||||
|
"retry": "Retry"
|
||||||
},
|
},
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"title": "Page Not Found",
|
"title": "Page Not Found",
|
||||||
@@ -120,7 +122,8 @@
|
|||||||
"exploreMore": "Explore More",
|
"exploreMore": "Explore More",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"searchWorks": "Search Generated Works",
|
"searchWorks": "Search Generated Works",
|
||||||
"searchWorksPlaceholder": "Enter keywords to search generated works"
|
"searchWorksPlaceholder": "Enter keywords to search generated works",
|
||||||
|
"noTags": "No recommended tags"
|
||||||
},
|
},
|
||||||
"templateDetail": {
|
"templateDetail": {
|
||||||
"title": "Hello, I'm a new resident of Zootopia 👋",
|
"title": "Hello, I'm a new resident of Zootopia 👋",
|
||||||
|
|||||||
@@ -93,7 +93,9 @@
|
|||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
"title": "全部频道",
|
"title": "全部频道",
|
||||||
"channelName": "频道名称"
|
"channelName": "频道名称",
|
||||||
|
"noCategories": "暂无分类",
|
||||||
|
"retry": "重新加载"
|
||||||
},
|
},
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"title": "页面未找到",
|
"title": "页面未找到",
|
||||||
@@ -120,7 +122,8 @@
|
|||||||
"exploreMore": "探索更多",
|
"exploreMore": "探索更多",
|
||||||
"refresh": "换一换",
|
"refresh": "换一换",
|
||||||
"searchWorks": "搜索生成的作品",
|
"searchWorks": "搜索生成的作品",
|
||||||
"searchWorksPlaceholder": "请输入关键词搜索生成的作品"
|
"searchWorksPlaceholder": "请输入关键词搜索生成的作品",
|
||||||
|
"noTags": "暂无推荐标签"
|
||||||
},
|
},
|
||||||
"templateDetail": {
|
"templateDetail": {
|
||||||
"title": "泥嚎 我是动物城的新居民 👋",
|
"title": "泥嚎 我是动物城的新居民 👋",
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
"lint": "expo lint"
|
"lint": "expo lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@repo/core": "1.0.1",
|
"@repo/core": "1.0.2",
|
||||||
"@repo/sdk": "1.0.4",
|
"@repo/sdk": "1.0.7",
|
||||||
"@better-auth/expo": "1.3.34",
|
"@better-auth/expo": "1.3.34",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@gorhom/bottom-sheet": "^5.2.8",
|
"@gorhom/bottom-sheet": "^5.2.8",
|
||||||
|
|||||||
Reference in New Issue
Block a user