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 React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Platform, 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 { useFileUpload } from '@/hooks/actions/use-file-upload' import { useTemplateActions } from '@/hooks/actions/use-template-actions' import { useAuth } from '@/hooks/core/use-auth' import { useTemplateGenerations } from '@/hooks/data/use-template-generations' import { screenWidth } from '@/utils' import { aniStorage } from '@/utils/aniStorage' import { cn } from '@/utils/cn' // ============ 小组件 ============ 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, isVisible, onSelect, }: { post: any isSelected: boolean isSelectionMode: boolean itemWidth: number isVisible: boolean 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 = memo(({ connectedDevice, onPick }: { connectedDevice: any; onPick: () => void }) => { return ( {connectedDevice ? '设备已连接' : '设备离线'} 上传本地 ) }) 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------------', url) if (!url) return null const Width = 256 return ( ) }) const ManagerView = memo( ({ viewState, onBack, onConnectToggle, }: { viewState: string onBack: () => void onConnectToggle: (device: any) => void }) => { const { discoveredDevices } = useBleExplorer() if (viewState !== 'manager') return null return ( 设备管理 {/* 空占位符 */} {discoveredDevices .filter((i) => i.name) .map((device) => ( ))} ) }, ) // ============ 主组件 ============ const Sync = () => { const { user } = useAuth() const { data: generationsData, loading: generationsLoading, load: loadGenerations } = useTemplateGenerations() const { uploadFile, loading: uploadLoading } = useFileUpload() const { runTemplate, batchDeleteGenerations, loading: actionLoading } = useTemplateActions() const [viewState, setViewState] = useState<'home' | 'manager' | 'scanner'>('home') const [selectedItem, setSelectedItem] = useState({} as any) const [isSelectionMode, setIsSelectionMode] = useState(false) const [selectedIds, setSelectedIds] = useState>(new Set()) const { connectedDevice, startScan, stopScan, connectToDevice, disconnectDevice, transferMediaSingle, } = useBleExplorer() const itemWidth = Math.floor((screenWidth - 12 * 2 - 12 * 2) / 3) // 加载生成记录 useEffect(() => { if (user?.id) { loadGenerations() } }, [user?.id, loadGenerations]) // 将生成记录转换为 posts 格式 const posts = useMemo(() => { const generations = generationsData?.data || [] return generations.map((gen: any) => ({ id: gen.id, imageUrl: Array.isArray(gen.resultUrl) ? gen.resultUrl[0] : gen.resultUrl, 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[0] const newItem = { id: firstItem.id, imageUrl: firstItem.imageUrl, url: firstItem.imageUrl, originalUrl: firstItem.originalUrl, } setSelectedItem(newItem) } }, [posts]) const viewableIds = useRef>(new Set(posts.map((p: any) => p.id))) // 事件处理函数 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 () => { // await aniStorage.set('test_url', { test: 'data' }) console.log( 'aniStorage.has----------------', await aniStorage.has('test_url'), // await aniStorage.delete('test_url'), // await aniStorage.get('test_url'), ) if (!canSync) { Toast.show({ title: '请先连接设备' }) return } Toast.show({ renderContent: () => ( 正在同步文件... ), duration: 0, }) transferMediaSingle(selectedItem?.imageUrl) .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 Toast.showLoading({ title: '上传中...', duration: 30e3 }) const file = { name: result.fileName, type: result.mimeType || 'image/jpeg', uri: Platform.OS === 'android' ? result.uri : result.uri.replace('file://', ''), } const formData = new FormData() formData.append('file', file as any) const { url, error } = await uploadFile(file as any) Toast.hideLoading() if (error || !url) { Toast.show({ title: '上传失败' }) return } const newItem = { id: `local-${Date.now()}`, // type: isVideo ? 'video' : 'image', imageUrl: url, url, originalUrl: url, // ...(typeof asset === 'object' ? asset : {}), } setSelectedItem(newItem) // // 如果设备已连接,询问是否同步 // 先预览不直接同步上传 // if (connectedDevice?.id) { // Toast.showModal( // 文件已上传,是否立即同步到设备?} // onConfirm={() => { // Toast.hideModal() // handleSync() // }} // onCancel={() => Toast.hideModal()} // />, // ) // } else { // Toast.show({ title: '上传成功' }) // } }, [uploadFile, connectedDevice, handleSync]) const startConnect = useCallback(() => { setViewState('manager') startScan() }, [startScan]) const handleGenAgain = useCallback(() => { if (!selectedItem?.templateId) { Toast.show({ title: '请先选择一个生成记录' }) return } Toast.showModal( 生成同款风格将消耗 2 Goo{' '} 算力。 } onCancel={() => Toast.hideModal()} onConfirm={handleGenAgainConfirm} />, ) }, [selectedItem]) const handleGenAgainConfirm = useCallback(async () => { if (!selectedItem?.templateId || !selectedItem?.originalUrl) return Toast.hideModal() Toast.show({ renderContent: () => ( 正在生成中... ), duration: 0, }) const { generationId, error } = await runTemplate({ templateId: selectedItem.templateId, data: {}, originalUrl: selectedItem.originalUrl, }) Toast.hideLoading() if (error || !generationId) { Toast.show({ title: error?.message || '生成失败' }) return } Toast.show({ renderContent: () => ( 生成成功 ), }) // 刷新列表 if (user?.id) { loadGenerations() } }, [selectedItem, runTemplate, user?.id, loadGenerations]) const toggleSelectionMode = useCallback(() => { setIsSelectionMode((v) => !v) setSelectedIds(new Set()) }, []) const handleItemSelect = useCallback( (post: any) => { 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(() => { if (selectedIds.size === posts.length) { setSelectedIds(new Set()) } else { setSelectedIds(new Set(posts.map((p: any) => p.id))) } }, [posts, selectedIds.size]) const onViewableItemsChanged = useCallback((params: any) => { const { viewableItems } = params viewableIds.current = new Set(viewableItems.map((v: any) => v.item.id)) }, []) 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 const isVisible = viewableIds.current.has(post.id) return ( ) }, [isSelectionMode, selectedIds, selectedItem, itemWidth, handleItemSelect], ) return ( 'row'} ItemSeparatorComponent={() => } keyExtractor={(item: any) => item?.id} ListHeaderComponent={renderHeader} numColumns={3} renderItem={renderGridItem} viewabilityConfig={{ itemVisiblePercentThreshold: 60 }} onViewableItemsChanged={onViewableItemsChanged} /> { setViewState('home') stopScan() }} /> ) } export default memo(Sync)