761 lines
29 KiB
TypeScript
761 lines
29 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from 'react'
|
||
import {
|
||
View,
|
||
Text,
|
||
StyleSheet,
|
||
ScrollView,
|
||
Dimensions,
|
||
Pressable,
|
||
StatusBar as RNStatusBar,
|
||
RefreshControl,
|
||
ActivityIndicator,
|
||
NativeScrollEvent,
|
||
NativeSyntheticEvent,
|
||
} from 'react-native'
|
||
import { StatusBar } from 'expo-status-bar'
|
||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||
import { Image } from 'expo-image'
|
||
import { useRouter } from 'expo-router'
|
||
import { useTranslation } from 'react-i18next'
|
||
import { PanGestureHandler } from 'react-native-gesture-handler'
|
||
import { PointsIcon, SearchIcon, SettingsIcon } from '@/components/icon'
|
||
import EditProfileDrawer from '@/components/drawer/EditProfileDrawer'
|
||
import Dropdown from '@/components/ui/dropdown'
|
||
import Toast from '@/components/ui/Toast'
|
||
import { signOut, useSession } from '@/lib/auth'
|
||
import {
|
||
useTemplateGenerations,
|
||
useUserFavorites,
|
||
useUserLikes,
|
||
type TemplateGeneration
|
||
} from '@/hooks'
|
||
import { MySkeleton } from '@/components/skeleton/MySkeleton'
|
||
import { TabNavigation } from '@/components/blocks/home/TabNavigation'
|
||
import { useSwipeNavigation } from '@/hooks/use-swipe-navigation'
|
||
|
||
import { useUserBalance } from '@/hooks/use-user-balance'
|
||
|
||
const { width: screenWidth } = Dimensions.get('window')
|
||
const GALLERY_GAP = 2
|
||
const GALLERY_HORIZONTAL_PADDING = 0
|
||
const GALLERY_ITEM_SIZE = Math.floor(
|
||
(screenWidth - GALLERY_HORIZONTAL_PADDING * 2 - GALLERY_GAP * 2) / 3
|
||
)
|
||
|
||
type TabType = 'works' | 'favorites' | 'likes'
|
||
|
||
// 获取作品封面图 - Webp优先
|
||
const getCoverUrl = (item: TemplateGeneration) =>
|
||
item.webpPreviewUrl || item.resultUrl?.[0] || item.template?.coverImageUrl
|
||
|
||
export default function My() {
|
||
const router = useRouter()
|
||
const { t, i18n } = useTranslation()
|
||
const [editDrawerVisible, setEditDrawerVisible] = useState(false)
|
||
|
||
// Tab 状态
|
||
const [activeTab, setActiveTab] = useState<TabType>('works')
|
||
const [activeTabIndex, setActiveTabIndex] = useState(0)
|
||
|
||
// 获取积分余额
|
||
const { balance } = useUserBalance()
|
||
|
||
// 获取当前登录用户信息
|
||
const { data: session } = useSession()
|
||
const userName = session?.user?.name || session?.user?.username || '用户'
|
||
const [profileName, setProfileName] = useState(userName)
|
||
|
||
// 当 session 变化时更新用户名
|
||
useEffect(() => {
|
||
if (session?.user?.name || session?.user?.username) {
|
||
setProfileName(session?.user?.name || session?.user?.username || '用户')
|
||
}
|
||
}, [session])
|
||
|
||
// 使用 useTemplateGenerations hook 获取用户作品列表
|
||
const {
|
||
generations,
|
||
loading,
|
||
loadingMore,
|
||
refetch,
|
||
loadMore,
|
||
hasMore,
|
||
} = useTemplateGenerations()
|
||
|
||
// 获取收藏列表
|
||
const {
|
||
favorites,
|
||
loading: favoritesLoading,
|
||
loadingMore: favoritesLoadingMore,
|
||
refetch: favoritesRefetch,
|
||
loadMore: favoritesLoadMore,
|
||
hasMore: favoritesHasMore,
|
||
} = useUserFavorites()
|
||
|
||
// 获取点赞列表
|
||
const {
|
||
likes,
|
||
loading: likesLoading,
|
||
loadingMore: likesLoadingMore,
|
||
refetch: likesRefetch,
|
||
loadMore: likesLoadMore,
|
||
hasMore: likesHasMore,
|
||
} = useUserLikes()
|
||
|
||
// Tab 配置
|
||
const tabs = [
|
||
t('my.tabs.works'),
|
||
t('my.tabs.favorites'),
|
||
t('my.tabs.likes')
|
||
]
|
||
|
||
// 处理 Tab 切换
|
||
const handleTabPress = useCallback((index: number) => {
|
||
setActiveTabIndex(index)
|
||
if (index === 0) setActiveTab('works')
|
||
else if (index === 1) setActiveTab('favorites')
|
||
else if (index === 2) setActiveTab('likes')
|
||
}, [])
|
||
|
||
// 左右滑动切换 Tab
|
||
const handleSwipeLeft = useCallback(() => {
|
||
if (activeTabIndex < tabs.length - 1) {
|
||
handleTabPress(activeTabIndex + 1)
|
||
}
|
||
}, [activeTabIndex, tabs.length, handleTabPress])
|
||
|
||
const handleSwipeRight = useCallback(() => {
|
||
if (activeTabIndex > 0) {
|
||
handleTabPress(activeTabIndex - 1)
|
||
}
|
||
}, [activeTabIndex, handleTabPress])
|
||
|
||
const { handleGestureEvent, handleGestureStateChange } = useSwipeNavigation({
|
||
onSwipeLeft: handleSwipeLeft,
|
||
onSwipeRight: handleSwipeRight,
|
||
canSwipeLeft: activeTabIndex < tabs.length - 1,
|
||
canSwipeRight: activeTabIndex > 0,
|
||
})
|
||
|
||
// 调试日志
|
||
useEffect(() => {
|
||
console.log('📊 作品列表状态:', {
|
||
总数: generations.length,
|
||
加载中: loading,
|
||
加载更多中: loadingMore,
|
||
还有更多: hasMore
|
||
})
|
||
}, [generations.length, loading, loadingMore, hasMore])
|
||
|
||
// 初始化加载数据
|
||
useEffect(() => {
|
||
refetch({ page: 1, limit: 20 })
|
||
favoritesRefetch()
|
||
likesRefetch()
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [])
|
||
|
||
// 下拉刷新状态
|
||
const [refreshing, setRefreshing] = useState(false)
|
||
const [favoritesRefreshing, setFavoritesRefreshing] = useState(false)
|
||
const [likesRefreshing, setLikesRefreshing] = useState(false)
|
||
|
||
// 防止重复触发加载更多
|
||
const isLoadingMoreRef = useRef(false)
|
||
const isFavoritesLoadingMoreRef = useRef(false)
|
||
const isLikesLoadingMoreRef = useRef(false)
|
||
|
||
// 下拉刷新处理
|
||
const onRefresh = useCallback(async () => {
|
||
setRefreshing(true)
|
||
try {
|
||
await refetch({ page: 1, limit: 20 })
|
||
} finally {
|
||
setRefreshing(false)
|
||
}
|
||
}, [refetch])
|
||
|
||
// 收藏Tab下拉刷新处理
|
||
const onFavoritesRefresh = useCallback(async () => {
|
||
setFavoritesRefreshing(true)
|
||
try {
|
||
await favoritesRefetch()
|
||
} finally {
|
||
setFavoritesRefreshing(false)
|
||
}
|
||
}, [favoritesRefetch])
|
||
|
||
// 点赞Tab下拉刷新处理
|
||
const onLikesRefresh = useCallback(async () => {
|
||
setLikesRefreshing(true)
|
||
try {
|
||
await likesRefetch()
|
||
} finally {
|
||
setLikesRefreshing(false)
|
||
}
|
||
}, [likesRefetch])
|
||
|
||
// 加载更多处理
|
||
const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent
|
||
const paddingToBottom = 200 // 距离底部200px时触发加载更多
|
||
|
||
const isCloseToBottom =
|
||
layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom
|
||
|
||
// 使用 ref 防止重复触发
|
||
if (isCloseToBottom && !loadingMore && hasMore && !loading && !isLoadingMoreRef.current) {
|
||
console.log('🔄 触发加载更多', {
|
||
layoutHeight: layoutMeasurement.height,
|
||
offsetY: contentOffset.y,
|
||
contentHeight: contentSize.height,
|
||
distance: contentSize.height - (layoutMeasurement.height + contentOffset.y)
|
||
})
|
||
isLoadingMoreRef.current = true
|
||
loadMore().finally(() => {
|
||
isLoadingMoreRef.current = false
|
||
})
|
||
}
|
||
}, [loadingMore, hasMore, loadMore, loading])
|
||
|
||
// 收藏Tab加载更多处理
|
||
const handleFavoritesScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent
|
||
const paddingToBottom = 200
|
||
|
||
const isCloseToBottom =
|
||
layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom
|
||
|
||
if (isCloseToBottom && !favoritesLoadingMore && favoritesHasMore && !favoritesLoading && !isFavoritesLoadingMoreRef.current) {
|
||
console.log('🔄 收藏Tab触发加载更多')
|
||
isFavoritesLoadingMoreRef.current = true
|
||
favoritesLoadMore().finally(() => {
|
||
isFavoritesLoadingMoreRef.current = false
|
||
})
|
||
}
|
||
}, [favoritesLoadingMore, favoritesHasMore, favoritesLoadMore, favoritesLoading])
|
||
|
||
// 点赞Tab加载更多处理
|
||
const handleLikesScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent
|
||
const paddingToBottom = 200
|
||
|
||
const isCloseToBottom =
|
||
layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom
|
||
|
||
if (isCloseToBottom && !likesLoadingMore && likesHasMore && !likesLoading && !isLikesLoadingMoreRef.current) {
|
||
console.log('🔄 点赞Tab触发加载更多')
|
||
isLikesLoadingMoreRef.current = true
|
||
likesLoadMore().finally(() => {
|
||
isLikesLoadingMoreRef.current = false
|
||
})
|
||
}
|
||
}, [likesLoadingMore, likesHasMore, likesLoadMore, likesLoading])
|
||
|
||
// 处理设置菜单选择
|
||
const handleSettingsSelect = async (value: string) => {
|
||
if (value === 'changePassword') {
|
||
router.push('/changePassword' as any)
|
||
} else if (value === 'language') {
|
||
// 切换语言
|
||
const newLang = i18n.language === 'zh-CN' ? 'en-US' : 'zh-CN'
|
||
i18n.changeLanguage(newLang)
|
||
} else if (value === 'logout') {
|
||
// 退出登录
|
||
console.log('🚪 点击退出登录')
|
||
const confirmText = i18n.language === 'zh-CN' ? '确定' : 'OK'
|
||
const cancelText = i18n.language === 'zh-CN' ? '取消' : 'Cancel'
|
||
const message = i18n.language === 'zh-CN' ? '确定要退出登录吗?' : 'Are you sure you want to logout?'
|
||
|
||
Toast.showActionSheet({
|
||
itemList: [message, confirmText, cancelText]
|
||
}).then(async (index) => {
|
||
// index 1 是确定按钮
|
||
if (index === 1) {
|
||
console.log('🚪 开始执行退出登录')
|
||
try {
|
||
Toast.showLoading({ title: i18n.language === 'zh-CN' ? '退出中...' : 'Logging out...' })
|
||
// 调用 better-auth 的 signOut 方法
|
||
await signOut()
|
||
Toast.hideLoading()
|
||
console.log('✅ 退出登录成功,跳转到登录页')
|
||
Toast.show(i18n.language === 'zh-CN' ? '退出登录成功' : 'Logged out successfully')
|
||
// 跳转到登录页面(注意:路由是 /auth 不是 /login)
|
||
router.replace('/auth')
|
||
} catch (error) {
|
||
Toast.hideLoading()
|
||
console.error('❌ 退出登录失败:', error)
|
||
Toast.show(i18n.language === 'zh-CN' ? '退出登录失败,请稍后重试' : 'Failed to logout, please try again later')
|
||
}
|
||
}
|
||
}).catch(() => {
|
||
console.log('❌ 取消退出登录')
|
||
})
|
||
}
|
||
}
|
||
|
||
// 设置菜单选项
|
||
const getLanguageLabel = () => {
|
||
if (i18n.language === 'zh-CN') {
|
||
return t('my.languageSwitch')
|
||
} else {
|
||
return t('my.languageSwitchEn')
|
||
}
|
||
}
|
||
|
||
const settingsOptions = [
|
||
{ label: t('my.changePassword'), value: 'changePassword' },
|
||
{ label: getLanguageLabel(), value: 'language' },
|
||
{ label: t('my.logout'), value: 'logout' },
|
||
]
|
||
|
||
return (
|
||
<SafeAreaView style={styles.container} edges={['top']}>
|
||
<StatusBar style="light" />
|
||
<RNStatusBar barStyle="light-content" />
|
||
|
||
{/* 顶部积分与设置 */}
|
||
<View style={styles.topBar}>
|
||
<Pressable
|
||
style={styles.pointsPill}
|
||
onPress={() => router.push('/membership' as any)}
|
||
>
|
||
<PointsIcon />
|
||
<Text style={styles.pointsPillText}>{balance}</Text>
|
||
</Pressable>
|
||
<Dropdown
|
||
options={settingsOptions}
|
||
onSelect={(value) => handleSettingsSelect(value)}
|
||
renderTrigger={(selectedOption, isOpen, toggle) => (
|
||
<Pressable onPress={toggle}>
|
||
<SettingsIcon />
|
||
</Pressable>
|
||
)}
|
||
dropdownStyle={{
|
||
minWidth: 160,
|
||
right: 10,
|
||
backgroundColor: '#2A2A2A80',
|
||
}}
|
||
/>
|
||
</View>
|
||
|
||
{/* 个人信息区 */}
|
||
<View style={styles.profileSection}>
|
||
<Image
|
||
source={session?.user?.image ? { uri: session.user.image } : require('@/assets/images/icon.png')}
|
||
style={styles.avatar}
|
||
contentFit="cover"
|
||
/>
|
||
<View style={styles.profileInfo}>
|
||
<Text style={styles.profileName}>{profileName}</Text>
|
||
<Text style={styles.profileSubTitle}>ID {session?.user?.id?.slice(0, 8) || '--------'}</Text>
|
||
</View>
|
||
<Pressable
|
||
style={styles.editButton}
|
||
onPress={() => {
|
||
console.log('[my.tsx] Edit Profile button pressed')
|
||
setEditDrawerVisible(true)
|
||
}}
|
||
>
|
||
<Text style={styles.editButtonText}>{t('my.editProfile')}</Text>
|
||
</Pressable>
|
||
</View>
|
||
|
||
{/* Tab 导航 */}
|
||
<TabNavigation
|
||
tabs={tabs}
|
||
activeIndex={activeTabIndex}
|
||
onTabPress={handleTabPress}
|
||
/>
|
||
|
||
{/* 作品九宫格 - 包裹在手势处理器中 */}
|
||
<PanGestureHandler
|
||
onGestureEvent={handleGestureEvent}
|
||
onHandlerStateChange={handleGestureStateChange}
|
||
activeOffsetX={[-20, 20]}
|
||
>
|
||
<View style={styles.gestureContainer}>
|
||
{activeTab === 'works' && loading ? (
|
||
<MySkeleton />
|
||
) : activeTab === 'works' ? (
|
||
<ScrollView
|
||
testID="my-scroll-view"
|
||
style={styles.scrollView}
|
||
contentContainerStyle={styles.scrollContent}
|
||
showsVerticalScrollIndicator={false}
|
||
onScroll={handleScroll}
|
||
scrollEventThrottle={16}
|
||
refreshControl={
|
||
<RefreshControl
|
||
refreshing={refreshing}
|
||
onRefresh={onRefresh}
|
||
tintColor="#9966FF"
|
||
colors={['#9966FF', '#FF6699', '#FF9966']}
|
||
progressBackgroundColor="#1C1E22"
|
||
progressViewOffset={10}
|
||
/>
|
||
}
|
||
>
|
||
<View style={styles.galleryGrid}>
|
||
{generations.map((item, index) => (
|
||
<Pressable
|
||
key={item.id}
|
||
style={[
|
||
styles.galleryItem,
|
||
index % 3 !== 2 && styles.galleryItemMarginRight,
|
||
styles.galleryItemMarginBottom,
|
||
]}
|
||
onPress={() => {
|
||
if (item.status === 'completed') {
|
||
router.push({
|
||
pathname: '/generationRecord' as any,
|
||
params: { id: item.id },
|
||
})
|
||
}
|
||
}}
|
||
disabled={item.status !== 'completed'}
|
||
>
|
||
<Image
|
||
source={getCoverUrl(item) ? { uri: getCoverUrl(item) } : require('@/assets/images/membership.png')}
|
||
style={styles.galleryImage}
|
||
contentFit="cover"
|
||
/>
|
||
|
||
{/* 遮罩:非完成状态 */}
|
||
{item.status !== 'completed' && (
|
||
<View style={styles.generatingOverlay} />
|
||
)}
|
||
|
||
{/* 数量角标:已完成且有结果 */}
|
||
{item.status === 'completed' && (item.resultUrl?.length || 0) > 0 && (
|
||
<View style={styles.counterBadge}>
|
||
<Text style={styles.counterText}>
|
||
{item.resultUrl?.length || 1}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
{/* 状态角标 */}
|
||
{item.status === 'running' && (
|
||
<View style={styles.generatingBadge}>
|
||
<Text style={styles.generatingBadgeText}>{t('my.generating')}</Text>
|
||
</View>
|
||
)}
|
||
{item.status === 'pending' && (
|
||
<View style={styles.generatingBadge}>
|
||
<Text style={styles.generatingBadgeText}>{t('my.queuing')}</Text>
|
||
</View>
|
||
)}
|
||
</Pressable>
|
||
))}
|
||
|
||
{/* 加载更多指示器 */}
|
||
{loadingMore && (
|
||
<View style={styles.loadingMoreContainer}>
|
||
<ActivityIndicator size="small" color="#9966FF" />
|
||
</View>
|
||
)}
|
||
|
||
{/* 空状态提示 */}
|
||
{!loading && generations.length === 0 && (
|
||
<View style={styles.emptyState}>
|
||
<Text style={styles.emptyStateText}>
|
||
{t('my.noWorks')}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</ScrollView>
|
||
) : activeTab === 'favorites' && favoritesLoading ? (
|
||
<MySkeleton />
|
||
) : activeTab === 'favorites' ? (
|
||
<ScrollView
|
||
style={styles.scrollView}
|
||
contentContainerStyle={styles.scrollContent}
|
||
showsVerticalScrollIndicator={false}
|
||
onScroll={handleFavoritesScroll}
|
||
scrollEventThrottle={16}
|
||
refreshControl={
|
||
<RefreshControl
|
||
refreshing={favoritesRefreshing}
|
||
onRefresh={onFavoritesRefresh}
|
||
tintColor="#9966FF"
|
||
colors={['#9966FF', '#FF6699', '#FF9966']}
|
||
progressBackgroundColor="#1C1E22"
|
||
progressViewOffset={10}
|
||
/>
|
||
}
|
||
>
|
||
<View style={styles.galleryGrid}>
|
||
{favorites.length === 0 && !favoritesLoading ? (
|
||
<View style={styles.emptyState}>
|
||
<Text style={styles.emptyStateText}>
|
||
{t('my.noFavorites')}
|
||
</Text>
|
||
</View>
|
||
) : (
|
||
<>
|
||
{favorites.map((item, index) => (
|
||
<Pressable
|
||
key={item.id}
|
||
style={[
|
||
styles.galleryItem,
|
||
index % 3 !== 2 && styles.galleryItemMarginRight,
|
||
styles.galleryItemMarginBottom,
|
||
]}
|
||
onPress={() => {
|
||
if (item.template?.id) {
|
||
router.push({
|
||
pathname: '/templateDetail' as any,
|
||
params: { id: item.template.id },
|
||
})
|
||
}
|
||
}}
|
||
>
|
||
<Image
|
||
source={item.template?.coverImageUrl ? { uri: item.template.coverImageUrl } : require('@/assets/images/membership.png')}
|
||
style={styles.galleryImage}
|
||
contentFit="cover"
|
||
/>
|
||
</Pressable>
|
||
))}
|
||
{/* 加载更多指示器 */}
|
||
{favoritesLoadingMore && (
|
||
<View style={styles.loadingMoreContainer}>
|
||
<ActivityIndicator size="small" color="#9966FF" />
|
||
</View>
|
||
)}
|
||
</>
|
||
)}
|
||
</View>
|
||
</ScrollView>
|
||
) : activeTab === 'likes' && likesLoading ? (
|
||
<MySkeleton />
|
||
) : activeTab === 'likes' ? (
|
||
<ScrollView
|
||
style={styles.scrollView}
|
||
contentContainerStyle={styles.scrollContent}
|
||
showsVerticalScrollIndicator={false}
|
||
onScroll={handleLikesScroll}
|
||
scrollEventThrottle={16}
|
||
refreshControl={
|
||
<RefreshControl
|
||
refreshing={likesRefreshing}
|
||
onRefresh={onLikesRefresh}
|
||
tintColor="#9966FF"
|
||
colors={['#9966FF', '#FF6699', '#FF9966']}
|
||
progressBackgroundColor="#1C1E22"
|
||
progressViewOffset={10}
|
||
/>
|
||
}
|
||
>
|
||
<View style={styles.galleryGrid}>
|
||
{likes.length === 0 && !likesLoading ? (
|
||
<View style={styles.emptyState}>
|
||
<Text style={styles.emptyStateText}>
|
||
{t('my.noLikes')}
|
||
</Text>
|
||
</View>
|
||
) : (
|
||
<>
|
||
{likes.map((item, index) => (
|
||
<Pressable
|
||
key={item.id}
|
||
style={[
|
||
styles.galleryItem,
|
||
index % 3 !== 2 && styles.galleryItemMarginRight,
|
||
styles.galleryItemMarginBottom,
|
||
]}
|
||
onPress={() => {
|
||
if (item.template?.id) {
|
||
router.push({
|
||
pathname: '/templateDetail' as any,
|
||
params: { id: item.template.id },
|
||
})
|
||
}
|
||
}}
|
||
>
|
||
<Image
|
||
source={item.template?.coverImageUrl ? { uri: item.template.coverImageUrl } : require('@/assets/images/membership.png')}
|
||
style={styles.galleryImage}
|
||
contentFit="cover"
|
||
/>
|
||
</Pressable>
|
||
))}
|
||
{/* 加载更多指示器 */}
|
||
{likesLoadingMore && (
|
||
<View style={styles.loadingMoreContainer}>
|
||
<ActivityIndicator size="small" color="#9966FF" />
|
||
</View>
|
||
)}
|
||
</>
|
||
)}
|
||
</View>
|
||
</ScrollView>
|
||
) : null}
|
||
</View>
|
||
</PanGestureHandler>
|
||
|
||
{/* 编辑资料抽屉 */}
|
||
<EditProfileDrawer
|
||
visible={editDrawerVisible}
|
||
onClose={() => setEditDrawerVisible(false)}
|
||
initialName={profileName}
|
||
initialAvatar={session?.user?.image}
|
||
onSave={(data) => {
|
||
setProfileName(data.name)
|
||
}}
|
||
/>
|
||
</SafeAreaView>
|
||
)
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: '#090A0B',
|
||
},
|
||
gestureContainer: {
|
||
flex: 1,
|
||
},
|
||
topBar: {
|
||
paddingHorizontal: GALLERY_HORIZONTAL_PADDING,
|
||
paddingTop: 19,
|
||
paddingRight: 16,
|
||
flexDirection: 'row',
|
||
justifyContent: 'flex-end',
|
||
backgroundColor: '#090A0B',
|
||
gap: 12,
|
||
},
|
||
pointsPill: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 1,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 4,
|
||
borderRadius: 100,
|
||
backgroundColor: '#1C1E22',
|
||
},
|
||
pointsPillText: {
|
||
color: '#FFCF00',
|
||
fontSize: 12,
|
||
fontWeight: '600',
|
||
},
|
||
scrollView: {
|
||
flex: 1,
|
||
backgroundColor: '#090A0B',
|
||
},
|
||
scrollContent: {
|
||
backgroundColor: '#090A0B',
|
||
paddingHorizontal: GALLERY_HORIZONTAL_PADDING,
|
||
paddingBottom: 100,
|
||
},
|
||
profileSection: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingLeft: 16,
|
||
paddingRight: 16,
|
||
marginTop: 20,
|
||
marginBottom: 32,
|
||
backgroundColor: '#090A0B',
|
||
},
|
||
avatar: {
|
||
width: 64,
|
||
height: 64,
|
||
borderRadius: 32,
|
||
overflow: 'hidden',
|
||
marginRight: 16,
|
||
},
|
||
profileInfo: {
|
||
flex: 1,
|
||
},
|
||
profileName: {
|
||
color: '#F5F5F5',
|
||
fontSize: 18,
|
||
fontWeight: '600',
|
||
marginBottom: 4,
|
||
},
|
||
profileSubTitle: {
|
||
color: '#FFFFFF',
|
||
fontSize: 12,
|
||
opacity: 0.7,
|
||
},
|
||
editButton: {
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 6,
|
||
borderRadius: 6,
|
||
backgroundColor: '#1C1E22',
|
||
},
|
||
editButtonText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 12,
|
||
},
|
||
galleryGrid: {
|
||
flexDirection: 'row',
|
||
flexWrap: 'wrap',
|
||
marginBottom: 10,
|
||
},
|
||
galleryItem: {
|
||
width: GALLERY_ITEM_SIZE,
|
||
aspectRatio: 1,
|
||
overflow: 'hidden',
|
||
backgroundColor: '#1C1E22',
|
||
position: 'relative',
|
||
},
|
||
galleryItemMarginRight: {
|
||
marginRight: GALLERY_GAP,
|
||
},
|
||
galleryItemMarginBottom: {
|
||
marginBottom: GALLERY_GAP,
|
||
},
|
||
galleryImage: {
|
||
width: '100%',
|
||
height: undefined,
|
||
aspectRatio: 1,
|
||
},
|
||
generatingOverlay: {
|
||
...StyleSheet.absoluteFillObject,
|
||
backgroundColor: '#00000080',
|
||
},
|
||
counterBadge: {
|
||
position: 'absolute',
|
||
right: 8,
|
||
bottom: 8,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 1,
|
||
borderRadius: 6,
|
||
backgroundColor: '#16181B1A',
|
||
},
|
||
counterText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 10,
|
||
fontWeight: '600',
|
||
},
|
||
generatingBadge: {
|
||
position: 'absolute',
|
||
left: 8,
|
||
bottom: 8,
|
||
},
|
||
generatingBadgeText: {
|
||
color: '#F5F5F5',
|
||
fontSize: 9,
|
||
fontWeight: '500',
|
||
},
|
||
loadingMoreContainer: {
|
||
width: '100%',
|
||
paddingVertical: 20,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
emptyState: {
|
||
width: '100%',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingVertical: 60,
|
||
},
|
||
emptyStateText: {
|
||
color: '#FFFFFF80',
|
||
fontSize: 14,
|
||
},
|
||
})
|