import { AntDesign, FontAwesome, Ionicons, MaterialCommunityIcons } from '@expo/vector-icons' import { Block, ConfirmModal, Text, Toast, VideoBox } from '@share/components' import Img from '@share/components/Img' import { FlashList } from '@shopify/flash-list' import * as ImagePicker from 'expo-image-picker' import { router, useFocusEffect } from 'expo-router' import { observer } from 'mobx-react-lite' import React, { memo, useCallback, useEffect, useMemo, useState } from 'react' import { ActivityIndicator, RefreshControl, ScrollView, View } from 'react-native' import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { imgPicker } from '@/@share/apis' import { useBleExplorer } from '@/ble' import BannerSection from '@/components/BannerSection' import { useTemplateActions } from '@/hooks/actions/use-template-actions' import { useTemplateGenerations } from '@/hooks/data/use-template-generations' import { userStore } from '@/stores' import { screenWidth, uploadFile } from '@/utils' import { cn } from '@/utils/cn' // ============ 主组件 ============ const Sync = observer(() => { // 从MobX Store获取用户信息 const { user } = userStore const { data: generationsData, loading: generationsLoading, loadingMore, load: loadGenerations, loadMore, refetch, } = useTemplateGenerations() const { reRunTemplate, batchDeleteGenerations, loading: actionLoading } = useTemplateActions() const [viewState, setViewState] = useState<'home' | 'manager' | 'scanner'>('home') const [selectedItem, setSelectedItem] = useState({ id: '', imageUrl: '', // url 是网络地址,本地预览使用 imageUrl url: '', originalUrl: '', templateId: '', }) const [isSelectionMode, setIsSelectionMode] = useState(false) const [selectedIds, setSelectedIds] = useState>(new Set()) const { connectedDevice, discoveredDevices, isScanning, startScan, stopScan, connectToDevice, disconnectDevice, transferMediaSingle, } = useBleExplorer() const itemWidth = Math.floor((screenWidth - 12 * 2 - 12 * 2) / 3) // 加载生成记录 useEffect(() => { if (user?.id) { loadGenerations() } }, [user?.id, loadGenerations]) // 页面聚焦时重新请求数据 useFocusEffect( useCallback(() => { if (user?.id) { console.log('页面聚焦,重新请求数据') loadGenerations() } }, [user?.id, loadGenerations]), ) // 将生成记录转换为 posts 格式 const posts = useMemo(() => { const generations = generationsData?.data || [] return generations .filter((gen: any) => gen?.id) // 过滤掉没有 id 的记录 .map((gen: any) => { const imageUrl = Array.isArray(gen?.resultUrl) ? gen?.resultUrl[0] : gen?.resultUrl return { id: gen?.id, imageUrl: imageUrl, url: imageUrl, originalUrl: gen?.originalUrl, templateId: gen?.templateId, type: gen?.type, status: gen?.status, createdAt: gen?.createdAt, title: `生成-${gen?.id.slice(0, 6)}`, rank: 'S', author: user?.name || 'User', avatarUrl: user?.image || 'https://image.pollinations.ai/prompt/cool%20anime%20boy%20avatar%20hoodie?seed=123&nologo=true', } }) }, [generationsData, user]) useEffect(() => { if (!selectedItem?.id && posts.length > 0) { const firstItem = posts.filter((p: any) => p.status === 'completed' || p.status === 'success')[0] if (firstItem?.id) { const newItem = { id: firstItem.id, imageUrl: firstItem.imageUrl, url: firstItem.url, originalUrl: firstItem.originalUrl, templateId: firstItem.templateId, } setSelectedItem(newItem) } } }, [posts]) // console.log('selectedItem-----------', selectedItem) // 事件处理函数 const handleConnectToggle = useCallback( async (item: any) => { if (item.connected) { disconnectDevice() } else { await disconnectDevice() Toast.showLoading({ title: '连接中...', duration: 30e3, }) connectToDevice(item) .then(() => { console.log('设备连接成功') }) .catch(() => { Toast.show({ title: '设备连接失败', }) }) .finally(() => { Toast.hideLoading() }) } }, [connectToDevice, disconnectDevice], ) const canSync = useMemo(() => { return !!connectedDevice?.id && !!selectedItem?.imageUrl }, [connectedDevice, selectedItem]) const handleSync = useCallback(async () => { console.log('selectedItem?.imageUrl-----------', selectedItem?.imageUrl) if (!canSync) { Toast.show({ title: '请先连接设备' }) return } Toast.show({ renderContent: () => ( 正在同步文件... ), duration: 0, }) transferMediaSingle(selectedItem?.url) .then(() => { Toast.show({ title: '同步成功' }) }) .catch(() => { Toast.hideLoading() Toast.show({ title: '同步失败' }) }) }, [canSync, selectedItem, transferMediaSingle]) const handlePick = useCallback(async () => { const assetList = await imgPicker({ maxImages: 1, type: ImagePicker.MediaTypeOptions.All, resultType: 'asset' }) if (!assetList?.length) return const result = assetList[0] as any console.log('result----------', result) // 先预览直接转 ani 同步 Toast.showLoading({ title: '上传中...', duration: 30e3 }) const url = await uploadFile({ uri: result.uri, mimeType: result.mimeType, fileName: result.fileName, }) console.log('handlePick------------', url) Toast.hideLoading() const newItem = { id: `local-${Date.now()}`, // type: isVideo ? 'video' : 'image', imageUrl: result?.uri, url: url, originalUrl: result?.uri, templateId: '', // ...(typeof asset === 'object' ? asset : {}), } setSelectedItem(newItem) }, [connectedDevice, handleSync]) const startConnect = useCallback(() => { setViewState('manager') startScan() }, [startScan]) // 当离开设备管理页面时停止扫描 useEffect(() => { if (viewState !== 'manager' && isScanning) { stopScan() } }, [viewState, isScanning, stopScan]) const handleGenAgain = useCallback(() => { if (!selectedItem?.templateId) { Toast.show({ title: '请先选择一个生成记录' }) return } Toast.showModal( 生成同款风格将消耗 2 Goo 算力。 } onCancel={() => Toast.hideModal()} onConfirm={handleGenAgainConfirm} />, ) }, [selectedItem]) const handleGenAgainConfirm = async () => { console.log('handleGenAgainConfirm selectedItem-----------', selectedItem) if (!selectedItem?.templateId || !selectedItem?.originalUrl) { Toast.show({ title: '请先选择一个生成记录' }) return } Toast.hideModal() const { generationId, error } = await reRunTemplate({ generationId: selectedItem?.id, }) Toast.hideLoading() if (error || !generationId) { Toast.show({ title: error?.message || '生成失败' }) return } Toast.show({ renderContent: () => ( 生成中... ), }) // 刷新列表 if (user?.id) { loadGenerations() } } const toggleSelectionMode = useCallback(() => { setIsSelectionMode((v) => !v) setSelectedIds(new Set()) }, []) const handleItemSelect = useCallback( (post: any) => { if (!post?.id) return // 防止访问 undefined 的 id if (isSelectionMode) { const next = new Set(selectedIds) if (next.has(post.id)) next.delete(post.id) else next.add(post.id) setSelectedIds(next) } else { setSelectedItem(post) } }, [isSelectionMode, selectedIds], ) const handleDelete = useCallback(async () => { if (selectedIds.size === 0) return const { success, error } = await batchDeleteGenerations(Array.from(selectedIds)) if (error || !success) { Toast.show({ title: error?.message || '删除失败' }) return } Toast.show({ title: `成功删除 ${selectedIds.size} 个生成记录` }) // 刷新列表 if (user?.id) { await loadGenerations() } setSelectedIds(new Set()) setIsSelectionMode(false) }, [selectedIds, batchDeleteGenerations, user?.id, loadGenerations]) const selectAll = useCallback(() => { const validPosts = posts.filter((p: any) => p?.id) // 过滤掉没有 id 的 posts if (selectedIds.size === validPosts.length) { setSelectedIds(new Set()) } else { setSelectedIds(new Set(validPosts.map((p: any) => p.id))) } }, [posts, selectedIds.size]) // 下拉刷新 const [refreshing, setRefreshing] = useState(false) const onRefresh = useCallback(async () => { setRefreshing(true) try { await refetch() } finally { setRefreshing(false) } }, [refetch]) // 加载更多 const onLoadMore = useCallback(() => { loadMore() }, [loadMore]) // 列表底部组件 const ListFooter = useMemo(() => { if (loadingMore) { return ( ) } return null }, [loadingMore]) const renderHeader = useMemo( () => ( ), [connectedDevice, handlePick, startConnect, isSelectionMode, toggleSelectionMode, selectedItem], ) const renderGridItem = useCallback( ({ item: post }: { item: any }) => { const isSelected = isSelectionMode ? selectedIds.has(post?.id) : selectedItem?.id === post?.id return ( ) }, [isSelectionMode, selectedIds, selectedItem, itemWidth, handleItemSelect], ) return ( 'row'} ItemSeparatorComponent={() => } keyExtractor={(item: any) => item?.id || `fallback-${Math.random()}`} ListFooterComponent={ListFooter} ListHeaderComponent={renderHeader} numColumns={3} renderItem={renderGridItem} refreshControl={ } onEndReached={onLoadMore} onEndReachedThreshold={0.3} /> { setViewState('home') stopScan() }} /> ) }) export default Sync // ============ 小组件 ============ const SpinningLoader = memo(() => { const rotation = useSharedValue(0) useEffect(() => { rotation.value = withRepeat(withTiming(360, { duration: 1000, easing: Easing.linear }), -1, false) }, []) const animatedStyle = useAnimatedStyle(() => ({ transform: [{ rotate: `${rotation.value}deg` }], })) return ( ) }) const DeviceItem = memo(({ device, onConnectToggle }: { device: any; onConnectToggle: (device: any) => void }) => { const { name = 'Unknown Device', id, connected: isConnected } = device return ( {isConnected && } {name} {isConnected ? '已连接' : '未连接'} onConnectToggle(device)} > {isConnected ? '已连接' : '连接'} ) }) const GridItem = memo( ({ post, isSelected, isSelectionMode, itemWidth, onSelect, }: { post: any isSelected: boolean isSelectionMode: boolean itemWidth: number onSelect: (post: any) => void }) => { // 渲染状态标记 const renderStatusBadge = () => { const status = post.status // 成功状态不显示标记 if (status === 'completed' || status === 'success') { return null } // 失败状态 if (status === 'failed') { return ( 失败 ) } // 其他状态显示加载中 return ( 生成中 ) } return ( onSelect(post)}> {isSelected && } {/* 状态标记 */} {renderStatusBadge()} {isSelectionMode && ( )} ) }, ) const FilterButtons = memo( ({ isSelectionMode, onToggleSelectionMode }: { isSelectionMode: boolean; onToggleSelectionMode: () => void }) => { return ( {['我的生成'].map((label) => { return ( {label} ) })} {isSelectionMode ? '取消' : '管理'} ) }, ) const SelectionBar = memo( ({ isSelectionMode, selectedCount, onSelectAll, onDelete, }: { isSelectionMode: boolean selectedCount: number onSelectAll: () => void onDelete: () => void }) => { const insets = useSafeAreaInsets() if (!isSelectionMode) return null return ( 已选: {selectedCount} 全选 0 ? 'bg-[#e61e25] text-white' : 'bg-gray-200 text-gray-400'}`} onClick={onDelete} > {selectedCount > 0 ? '删除' : '删除'} ) }, ) const FABButtons = memo( ({ onGenAgain, onSync, canSync }: { onGenAgain: () => void; onSync: () => void; canSync: boolean }) => { const insets = useSafeAreaInsets() return ( 再次生成 同步 ) }, ) // ============ 主要组件部分 ============ const HeaderBanner = observer(({ connectedDevice, onPick }: { connectedDevice: any; onPick: () => void }) => { const { user, isAuthenticated, signOut, session } = userStore // console.log('user-----------', user) // console.log('session--------', session) const handleLogout = () => { if (isAuthenticated) { signOut().then(() => { Toast.show({ title: '已登出' }) }) } else { router.replace('/auth') } } const loginText = isAuthenticated ? '登出' : '登录' return ( {connectedDevice ? '设备已连接' : '设备离线'} {loginText} 上传本地 ) }) const TopCircleSection = memo( ({ connectedDevice, onStartConnect, selectedItem, }: { connectedDevice: any onStartConnect: () => void selectedItem: any }) => { const outerRotate = useSharedValue(0) useEffect(() => { outerRotate.value = withRepeat(withTiming(360, { duration: 12000, easing: Easing.linear }), -1, false) }, []) const outerRingStyle = useAnimatedStyle(() => ({ transform: [{ rotate: `${outerRotate.value}deg` }], })) return ( {connectedDevice ? '已连接' : '选择设备'} {connectedDevice && ( {/* */} 离线模式 )} ) }, ) const GalleryRenderer = memo(({ selectedItem }: { selectedItem: any }) => { const url = selectedItem?.url || selectedItem?.imageUrl // console.log('GalleryRenderer--------------', selectedItem) if (!url) return null const Width = 256 return ( ) }) const ManagerView = memo( ({ viewState, onBack, onConnectToggle, discoveredDevices, isScanning, }: { viewState: string onBack: () => void onConnectToggle: (device: any) => void discoveredDevices: any[] isScanning: boolean }) => { console.log('isScanning------------', isScanning) console.log('discoveredDevices------------', discoveredDevices) if (viewState !== 'manager') return null const deviceList = discoveredDevices.filter((i) => i.name) return ( 设备管理 {/* 空占位符 */} 正在扫描设备... {deviceList.map((device) => ( ))} ) }, )