import { AntDesign, EvilIcons, FontAwesome, Ionicons, MaterialCommunityIcons } from '@expo/vector-icons' import { Block, ConfirmModal, Input, ListEmpty, 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, useRef, 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 { bleManager } from '@/ble/managers/bleManager' import BannerSection from '@/components/BannerSection' import { useTemplateActions } from '@/hooks/actions/use-template-actions' import { useTemplateGenerations } from '@/hooks/data/use-template-generations' import { bleStore, userStore } from '@/stores' import { screenWidth, uploadFile } from '@/utils' import { cn } from '@/utils/cn' import { extractCdnKey } from '@/utils/getCDNKey' // ============ 主组件 ============ const Sync = observer(() => { // 从MobX Store获取用户信息 const { user, isAuthenticated } = 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: '', asset: {}, price: 0, }) const [isSelectionMode, setIsSelectionMode] = useState(false) const [selectedIds, setSelectedIds] = useState>(new Set()) const { connectedDevice, isScanning, transferProgress } = bleStore.state const itemWidth = Math.floor((screenWidth - 12 * 2 - 12 * 2) / 3) useFocusEffect(() => { if (!isAuthenticated) { router.replace('/') router.push('/auth') } }) // 加载生成记录 useEffect(() => { if (!isAuthenticated) 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, price: gen?.template?.price || -1, 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, asset: firstItem?.asset || {}, price: firstItem?.price || -1, } setSelectedItem(newItem) } } }, [posts]) // console.log('selectedItem-----------', selectedItem) 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 } // 先预览直接转 ani 同步 let fileUrl = selectedItem?.url if (!selectedItem?.url && selectedItem?.asset?.uri) { Toast.showLoading({ title: '上传中...', duration: 30e3 }) const result = selectedItem?.asset const url = await uploadFile({ uri: result.uri, mimeType: result.mimeType, fileName: result.fileName, }) fileUrl = url setSelectedItem((prev) => ({ ...prev, url: fileUrl })) Toast.hideLoading() } console.log('handlePick------------', fileUrl) bleStore.setState((prestate) => { return { ...prestate, transferProgress: 0 } }) Toast.show({ renderContent: () => , duration: 0, }) bleManager .transferMediaSingle(fileUrl) .then(() => { const key = extractCdnKey(fileUrl) bleStore.addGalleryItem(key!) Toast.show({ title: '同步成功' }) }) .catch((e) => { Toast.hideLoading() console.log('e--------', e) Toast.show({ title: e || '同步失败' }) }) }, [canSync, selectedItem]) 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) const newItem = { id: `local-${Date.now()}`, // type: isVideo ? 'video' : 'image', imageUrl: result?.uri, url: '', originalUrl: result?.uri, templateId: '', asset: result, price: -1, } setSelectedItem(newItem) }, [connectedDevice, handleSync]) const startConnect = useCallback(() => { setViewState('manager') bleManager.startScan() }, []) // 当离开设备管理页面时停止扫描 useEffect(() => { if (viewState !== 'manager' && isScanning) { bleManager.stopScan() } }, [viewState, isScanning]) const handleGenAgain = useCallback(() => { console.log('selectedItem==========', selectedItem) if (!selectedItem?.templateId) { Toast.show({ title: '请先选择一个生成记录' }) return } const price = selectedItem?.price if (price <= 0) { Toast.show({ title: '请先上传' }) return } Toast.showModal( 生成同款风格将消耗 {price} 算力。 } onCancel={() => Toast.hideModal()} onConfirm={handleGenAgainConfirm} />, ) }, [selectedItem]) const handleGenAgainConfirm = async () => { console.log('handleGenAgainConfirm selectedItem-----------', selectedItem) if (!selectedItem?.templateId) { 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(() => { return ( ) }, [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} ListEmptyComponent={} numColumns={3} renderItem={renderGridItem} refreshControl={ } onEndReached={onLoadMore} onEndReachedThreshold={0.3} /> { setViewState('home') bleManager.stopScan() }} /> ) }) export default Sync // ============ 小组件 ============ const UpdateNameModal = ({ initialName, onNameChange, onConfirm, onCancel }: any) => { const [name, setName] = useState(initialName) useEffect(() => { setName(initialName) }, [initialName]) const handleChange = (text: string) => { setName(text) onNameChange && onNameChange(text) } return ( } onConfirm={onConfirm} onCancel={onCancel} /> ) } const SyncProgressToast = observer(() => { const { transferProgress } = bleStore.state const progressHint = Number(transferProgress) <= 0 ? `正在同步文件` : `正在同步文件,进度 ${transferProgress.toFixed(2)}%` return ( {progressHint} ) }) 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 = observer(({ device }: { device: any }) => { const { name = 'Unknown Device', id, connected: isConnected } = device const bindDevice = bleStore.bindDeviceList.find((d) => d?.id === id) const [nameShow, setNameShow] = useState(bindDevice ? `${bindDevice?.name}` : name) const tempName = useRef(nameShow) const canEdit = !!bindDevice && isConnected const { id: userId } = userStore.user || {} const onConnectToggle = async (device: any) => { if (device.connected) { bleManager.disconnectDevice() } else { await bleManager.disconnectDevice() Toast.showLoading({ title: '连接中...', duration: 30e3, }) bleManager .connectToDevice(device) .then(() => { console.log('设备连接成功') if (userId) { bleManager.bindDevice(userId) } }) .catch(() => { Toast.show({ title: '设备连接失败', }) }) .finally(() => { Toast.hideLoading() }) } } const handleUnbindDevice = async (device: any) => { if (!userId) { Toast.show({ title: '用户未登录' }) return } Toast.showModal( Toast.hideModal()} onConfirm={() => { Toast.hideModal() unbindDevice(device) }} />, ) } const unbindDevice = async (device: any) => { Toast.showLoading({ title: '解绑中...', duration: 30e3 }) try { await bleManager.unBindDevice(userId!) bleStore.removeBindDeviceItem(device.id) setNameShow(device.name || 'Unknown Device') Toast.show({ title: '解绑成功' }) } catch (error: any) { Toast.show({ title: `解绑失败: ${error?.message || error}` }) } finally { Toast.hideLoading() } } const handleUpdateName = () => { if (!canEdit) return Toast.showModal( { if (text.length > 12) { Toast.show({ title: '设备名称不能超过12个字符' }) return } tempName.current = text }} onConfirm={() => { console.log('onconfirm-----------', tempName) if (bindDevice) { bleStore.updateBindDeviceItem({ ...bindDevice, name: tempName.current }) setNameShow(tempName.current) } Toast.hideModal() }} onCancel={() => { Toast.hideModal() tempName.current = nameShow }} />, ) } return ( {isConnected && } {nameShow} {canEdit && } {isConnected ? '已连接' : '未连接'} {canEdit && ( handleUnbindDevice(device)} > 解绑 )} 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 = observer( ({ isSelectionMode, onToggleSelectionMode }: { isSelectionMode: boolean; onToggleSelectionMode: () => void }) => { const { isConnected } = bleStore.state return ( {['我的生成'].map((label) => { return ( {label} ) })} {isConnected && ( { router.push('/device') }} > {'吧唧管理'} )} {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 } = userStore // console.log('user-----------', user) // console.log('session--------', session) const handleLogout = () => { if (isAuthenticated) { signOut().then(() => { Toast.show({ title: '已登出' }) // router.replace('/') }) } } 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 = observer(({ viewState, onBack }: { viewState: string; onBack: () => void }) => { const { isScanning, discoveredDevices } = bleStore.state console.log('isScanning------------', isScanning) console.log('discoveredDevices------------', discoveredDevices) if (viewState !== 'manager') return null const deviceList = discoveredDevices.filter((i) => i.name) return ( 设备管理 {/* 空占位符 */} 正在扫描设备... {deviceList.map((device) => ( ))} ) })