This commit is contained in:
imeepos
2026-01-16 15:16:49 +08:00
parent fce99a57bf
commit dcdab410c6
11 changed files with 292 additions and 124 deletions

0
.env Normal file
View File

View File

@@ -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 }}

View File

@@ -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,

View File

@@ -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',
},
}) })

View File

@@ -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=="],

View File

@@ -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'

View 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,
}
}

View File

@@ -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) => ({

View File

@@ -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 👋",

View File

@@ -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": "泥嚎 我是动物城的新居民 👋",

View File

@@ -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",