expo-duooomi-app/app/(tabs)/sync.tsx

1014 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { AntDesign, EvilIcons, FontAwesome, Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'
import { useIsFocused } from '@react-navigation/native'
import { Block, ConfirmModal, Img, Input, ListEmpty, Text, Toast, VideoBox } from '@share/components'
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 ITEM_WIDTH = Math.floor((screenWidth - 12 * 2 - 12 * 2) / 3)
// ============ 主组件 ============
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<Set<string>>(new Set())
// 直接使用 useIsFocused hook页面失焦时不渲染列表项以减少内存占用
const isFocused = useIsFocused()
const { connectedDevice, isScanning, transferProgress } = bleStore.state
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 () => {
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: () => <SyncProgressToast />,
duration: 0,
})
bleManager
.transferMediaSingle(fileUrl)
.then(() => {
const key = extractCdnKey(fileUrl)
bleStore.addGalleryItem(key!)
Toast.show({ title: '同步成功' })
})
.catch((e) => {
Toast.hideLoading()
console.log('transferMediaSingle 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(
<ConfirmModal
title="确认生成同款风格吗?"
content={
<Text className="font-[bol]d text-[14px] leading-relaxed text-gray-800">
<Text className="font-[bla]ck mx-[4px] text-[20px] text-[#e61e25]">{price} </Text>
</Text>
}
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: () => (
<Block
className="flex w-full flex-row items-center justify-center border-[3px] border-white bg-black"
style={{ paddingVertical: 14, transform: [{ skewX: '-6deg' }] }}
>
<AntDesign color="white" name="check-circle" size={20} />
<Block style={{ marginLeft: 8 }}>
<Text className="font-[bla]ck text-[14px] text-white">...</Text>
</Block>
</Block>
),
})
// 刷新列表
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 (
<Block className="items-center justify-center py-[20px]">
<ActivityIndicator color="#FFE500" size="small" />
</Block>
)
}
return null
}, [loadingMore])
const renderHeader = useMemo(() => {
return (
<Block className="z-10">
<HeaderBanner connectedDevice={connectedDevice} onPick={handlePick} />
<TopCircleSection connectedDevice={connectedDevice} selectedItem={selectedItem} onStartConnect={startConnect} />
<FilterButtons isSelectionMode={isSelectionMode} onToggleSelectionMode={toggleSelectionMode} />
</Block>
)
}, [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 (
<GridItem
isSelected={isSelected}
isSelectionMode={isSelectionMode}
itemWidth={ITEM_WIDTH}
post={post}
// 页面失焦时不渲染减少内存占用
isVisible={isFocused}
onSelect={handleItemSelect}
/>
)
},
[isSelectionMode, selectedIds, selectedItem, handleItemSelect, isFocused],
)
return (
<Block className="relative flex-1 bg-black">
<BannerSection />
<Block className="z-10 flex-1">
<FlashList
contentContainerStyle={{ paddingHorizontal: 12, paddingBottom: 200 }}
data={posts}
drawDistance={300}
maxItemsInRecyclePool={0}
removeClippedSubviews={true}
ItemSeparatorComponent={() => <Block style={{ height: 6 }} />}
keyExtractor={(item: any) => item?.id || `fallback-${Math.random()}`}
ListFooterComponent={ListFooter}
ListHeaderComponent={renderHeader}
ListEmptyComponent={<ListEmpty />}
numColumns={3}
renderItem={renderGridItem}
refreshControl={
<RefreshControl colors={['#FFE500']} refreshing={refreshing} tintColor="#FFE500" onRefresh={onRefresh} />
}
onEndReached={onLoadMore}
onEndReachedThreshold={0.3}
/>
</Block>
<FABButtons canSync={canSync} onGenAgain={handleGenAgain} onSync={handleSync} />
<SelectionBar
isSelectionMode={isSelectionMode}
selectedCount={selectedIds.size}
onDelete={handleDelete}
onSelectAll={selectAll}
/>
<ManagerView
viewState={viewState}
onBack={() => {
setViewState('home')
bleManager.stopScan()
}}
/>
</Block>
)
})
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 (
<ConfirmModal
title="修改设备名称"
content={
<Block className="w-full gap-[12px] border">
<Input className="w-full" placeholder="请输入设备名称" value={name} onChangeText={handleChange} />
</Block>
}
onConfirm={onConfirm}
onCancel={onCancel}
/>
)
}
const SyncProgressToast = observer(() => {
const { transferProgress } = bleStore.state
const progressHint =
Number(transferProgress) <= 0 ? `正在同步文件` : `正在同步文件,进度 ${transferProgress.toFixed(2)}%`
return (
<Block
className="flex w-full flex-row items-center justify-center border-[3px] border-white bg-black"
style={{ paddingVertical: 14, transform: [{ skewX: '-6deg' }] }}
>
<SpinningLoader />
<Block style={{ marginLeft: 8 }}>
<Text className="font-[bla]ck text-[14px] text-white">{progressHint}</Text>
</Block>
</Block>
)
})
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 (
<Animated.View style={animatedStyle}>
<FontAwesome color="#FFE500" name="circle-o-notch" size={20} />
</Animated.View>
)
})
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(
<ConfirmModal
title="解绑设备"
content={`是否解绑设备?`}
onCancel={() => Toast.hideModal()}
onConfirm={() => {
Toast.hideModal()
unbindDevice(device)
bleManager.disconnectDevice()
}}
/>,
)
}
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(
<UpdateNameModal
initialName={tempName.current}
onNameChange={(text: string) => {
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 (
<Block className="relative mb-[16px] flex-row items-center overflow-hidden border-[3px] border-black bg-white p-[16px] shadow-large-black">
{isConnected && <Block className="absolute inset-y-0 left-0 w-[16px] border-r-[3px] border-black bg-accent" />}
<Block className="size-[48px] items-center justify-center border-[3px] border-black bg-gray-100 shadow-soft-black-10">
<Ionicons color="black" name="phone-portrait-outline" size={24} />
</Block>
<Block className="relative z-10 ml-[16px] flex-1 flex-row items-center justify-between gap-[16px]">
<Block className="flex-1">
<Block className="flex-1 flex-row items-center gap-[8px]" onClick={handleUpdateName}>
<Block className="flex-row flex-wrap items-center">
<Text className="break-words text-[18px] font-[900]">{nameShow}</Text>
{canEdit && <EvilIcons name="pencil" size={24} color="black" />}
</Block>
</Block>
<Block className="relative z-10 mt-[4px] flex-row items-center">
<Block className="flex-row">
<Block className="">
<Text
className={`border-2 border-black px-[6px] text-[10px] font-[900] ${isConnected ? 'bg-black text-accent' : 'bg-gray-200 text-gray-500'}`}
>
{isConnected ? '已连接' : '未连接'}
</Text>
</Block>
</Block>
{canEdit && (
<Block
className="ml-[8px] border-2 border-black bg-black px-[6px]"
style={{ marginLeft: 8 }}
onClick={() => handleUnbindDevice(device)}
>
<Text className={`text-[10px] font-[900] text-accent`}></Text>
</Block>
)}
</Block>
</Block>
<Block className="relative z-10 flex-row items-center space-x-[8px]">
<Block
className={`h-[40px] items-center justify-center border-[3px] border-black px-[16px] text-[12px] font-[900] ${isConnected ? 'bg-accent text-black shadow-medium-black' : 'bg-white text-black shadow-medium-gray'}`}
onClick={() => onConnectToggle(device)}
>
<Text className="text-[12px] font-[900]">{isConnected ? '已连接' : '连接'}</Text>
</Block>
</Block>
</Block>
</Block>
)
})
const GridItem = memo(
({
post,
isSelected,
isSelectionMode,
itemWidth,
isVisible = true,
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 (
<Block
className="absolute left-[8px] top-[8px] z-20 flex-row items-center gap-[4px] border-2 border-black bg-[#e61e25] px-[8px] py-[4px] shadow-hard-black"
style={{ transform: [{ skewX: '6deg' }] }}
>
<Ionicons color="white" name="close-circle" size={14} />
<Text className="text-[10px] font-[900] text-white"></Text>
</Block>
)
}
// 其他状态显示加载中
return (
<Block
className="absolute left-[8px] top-[8px] z-20 flex-row items-center gap-[4px] border-2 border-black bg-white px-[8px] py-[4px] shadow-hard-black"
style={{ transform: [{ skewX: '6deg' }] }}
>
<SpinningLoader />
<Text className="text-[10px] font-[900] text-black"></Text>
</Block>
)
}
return (
<Block className="relative" key={post?.id} onClick={() => onSelect(post)}>
<Block
className={`relative overflow-hidden border-2 ${isSelected ? 'shadow-[0px_0px_0px_4px_#FFE500]' : 'shadow-hard-black'} ${isSelected ? 'border-accent' : 'border-black'}`}
style={{ transform: [{ skewX: '-6deg' }], height: itemWidth, width: itemWidth }}
>
<Block style={{ height: itemWidth, width: itemWidth }}>
{isVisible && <Img className="size-full" src={post.imageUrl} />}
</Block>
{isSelected && <Block className="absolute inset-0 border-[3px] border-accent" />}
{/* 状态标记 */}
{renderStatusBadge()}
{isSelectionMode && (
<Block
className="absolute inset-0 z-30 items-center justify-center"
style={{ transform: [{ skewX: '6deg' }] }}
>
<Block
className={`size-[32px] items-center justify-center rounded-full border-[3px] border-black ${isSelected ? 'bg-accent' : 'bg-white/50'}`}
>
<Ionicons color="black" name="checkmark" size={20} />
</Block>
</Block>
)}
</Block>
</Block>
)
},
)
const FilterButtons = observer(
({ isSelectionMode, onToggleSelectionMode }: { isSelectionMode: boolean; onToggleSelectionMode: () => void }) => {
const { isConnected } = bleStore.state
return (
<Block className="flex flex-row items-center justify-between" style={{ marginVertical: 16 }}>
<Block className="flex-row">
{['我的生成'].map((label) => {
return (
<Block
key={label}
className={`-skew-x-3 border-2 border-black bg-black px-[16px] py-[4px] shadow-small-accent`}
>
<Text className={`text-[10px] font-[900] text-accent`}>{label}</Text>
</Block>
)
})}
</Block>
<Block className="flex-row items-center gap-[12px]">
{isConnected && (
<Block
className={`border-2 border-black bg-accent px-[16px] py-[6px] text-white shadow-medium-black`}
onClick={() => {
router.push('/device')
}}
>
<Text className="text-[12px] font-[900]">{'吧唧管理'}</Text>
</Block>
)}
<Block
className={`border-2 border-black px-[16px] py-[6px] shadow-medium-black ${isSelectionMode ? 'bg-[#e61e25] text-white' : 'bg-accent text-black'}`}
onClick={onToggleSelectionMode}
>
<Text className="text-[12px] font-[900]">{isSelectionMode ? '取消' : '管理'}</Text>
</Block>
</Block>
</Block>
)
},
)
const SelectionBar = memo(
({
isSelectionMode,
selectedCount,
onSelectAll,
onDelete,
}: {
isSelectionMode: boolean
selectedCount: number
onSelectAll: () => void
onDelete: () => void
}) => {
const insets = useSafeAreaInsets()
if (!isSelectionMode) return null
return (
<Block className="absolute inset-x-[16px] bottom-[96px] z-50" style={{ paddingBottom: insets.bottom }}>
<Block className="-skew-x-3 flex-row items-center justify-between border-[3px] border-black bg-white p-[12px] shadow-large-black">
<Block className="skew-x-3 flex-row items-center gap-[12px] pl-[8px]">
<Text className="text-[14px] font-[900]">: {selectedCount}</Text>
<Block className="text-[12px] underline" onClick={onSelectAll}>
<Text className="text-[12px] font-[700]"></Text>
</Block>
</Block>
<Block
className={`skew-x-3 flex-row items-center space-x-[8px] border-2 border-black px-[16px] py-[8px] font-[[90]0] text-[14px] shadow-medium-black${selectedCount > 0 ? 'bg-[#e61e25] text-white' : 'bg-gray-200 text-gray-400'}`}
onClick={onDelete}
>
<Ionicons color="black" name="trash-outline" size={16} />
<Text className="text-[14px] font-[900]">{selectedCount > 0 ? '删除' : '删除'}</Text>
</Block>
</Block>
</Block>
)
},
)
const FABButtons = memo(
({ onGenAgain, onSync, canSync }: { onGenAgain: () => void; onSync: () => void; canSync: boolean }) => {
const insets = useSafeAreaInsets()
return (
<Block className="absolute inset-x-[16px] bottom-[96px] z-40" style={{ paddingBottom: insets.bottom }}>
<Block className="w-full flex-row gap-[12px]">
<Block className="flex-1 -skew-x-6 shadow-large-black">
<Block
className={cn`relative h-[56px] items-center justify-center overflow-hidden border-[2px] border-black bg-[#e61e25] shadow-large-accent`}
onClick={onGenAgain}
>
<Block className="z-10 skew-x-6 flex-row items-center gap-[8px]">
<MaterialCommunityIcons color="white" name="star-four-points" size={20} />
<Text className="font-[bol]d text-[16px] text-white"></Text>
</Block>
</Block>
</Block>
<Block className="flex-1 -skew-x-6 shadow-large-accent">
<Block
className={cn`relative h-[56px] items-center justify-center overflow-hidden border-[2px] border-black bg-black`}
onClick={onSync}
>
<Block className="z-10 flex-row items-center gap-[8px]">
<Ionicons color={'white'} name="refresh" size={20} />
<Text className={cn`font-[bol]d bg-black text-[16px] text-white`}></Text>
</Block>
</Block>
</Block>
</Block>
</Block>
)
},
)
// ============ 主要组件部分 ============
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 (
<Block className="relative z-40 flex-row items-center justify-between py-[12px]">
<Block className="flex-row items-center gap-[8px] border-2 border-white bg-black py-[4px] shadow-hard-accent">
<Block className={`size-[8px] ${connectedDevice ? 'bg-accent' : 'bg-red-500'}`} />
<Text className="text-[12px] font-[900] tracking-[1px] text-white">
{connectedDevice ? '设备已连接' : '设备离线'}
</Text>
</Block>
<Block className="flex-row">
<Block
className="!mr-[12px] h-[40px] flex-row items-center justify-center gap-[8px] border-[3px] border-black bg-white px-[12px] shadow-hard-black"
onClick={handleLogout}
>
<Text className="text-[12px] font-[900] text-black">{loginText}</Text>
</Block>
<Block
className="h-[40px] flex-row items-center justify-center gap-[8px] border-[3px] border-black bg-white px-[12px] shadow-hard-black"
onClick={onPick}
>
<Ionicons color="black" name="cloud-upload-outline" size={18} />
<Text className="text-[12px] font-[900] text-black"></Text>
</Block>
</Block>
</Block>
)
})
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 (
<Block className="relative z-10 items-center">
<Block className="relative my-[24px] flex w-full flex-row items-center justify-between">
<Block
className={`w-[48px] items-center justify-center gap-[4px] border-[3px] border-black py-[12px] shadow-hard-black-50 ${connectedDevice ? 'bg-accent' : 'bg-white'}`}
onClick={onStartConnect}
>
<Ionicons
color={connectedDevice ? 'black' : '#6B7280'}
name={connectedDevice ? 'wifi' : 'wifi-outline'}
size={20}
/>
<Text className="text-[10px] font-[900]">{connectedDevice ? '已连接' : '选择设备'}</Text>
</Block>
<Block className="relative size-[256px] items-center justify-center">
<Block
animated
className="absolute inset-[-12px] rounded-full border-[3px] border-dashed border-black opacity-30"
style={outerRingStyle}
/>
<Block>
<GalleryRenderer selectedItem={selectedItem} />
{connectedDevice && (
<Block className="absolute inset-0 items-center justify-center">
{/* <Ionicons name="desktop-outline" size={48} color="white" /> */}
<Text className="border border-white bg-black px-[12px] py-[2px] text-[12px] font-[900] text-white">
线
</Text>
</Block>
)}
</Block>
</Block>
<Block className="h-[10px] w-[48px]" />
</Block>
</Block>
)
},
)
const GalleryRenderer = memo(({ selectedItem }: { selectedItem: any }) => {
const url = selectedItem?.url || selectedItem?.imageUrl
// console.log('GalleryRenderer--------------', selectedItem)
if (!url) return null
const Width = 256
return (
<View
className="relative z-10 border-4 border-black"
style={{ width: Width, height: Width, borderRadius: Width, overflow: 'hidden' }}
>
<VideoBox style={{ width: Width, height: Width }} width={Width} url={url} />
</View>
)
})
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 (
<Block className="absolute inset-0" style={{ zIndex: 100 }}>
<Block className="relative flex-1 bg-paper">
<Block className="flex-row items-center justify-between px-[16px] py-[12px]">
<Block
className="size-[40px] items-center justify-center border-[3px] border-black bg-white shadow-medium-black"
onClick={onBack}
>
<Ionicons color="black" name="chevron-back" size={24} />
</Block>
<Text className="text-[20px] font-[900] tracking-[-0.5px]"></Text>
<Block className="size-[40px] items-center justify-center bg-black/0">{/* 空占位符 */}</Block>
</Block>
<ScrollView className="relative flex-1 bg-paper">
<Block className="p-[16px]">
<Block className="mb-[16px] flex-row items-center gap-[12px] border-[3px] border-black bg-white p-[16px] shadow-medium-black">
<SpinningLoader />
<Text className="text-[14px] font-[900] text-black">...</Text>
</Block>
{deviceList.map((device) => (
<DeviceItem key={device.id} device={device} />
))}
</Block>
<Block className="h-[200px] w-full" />
</ScrollView>
</Block>
</Block>
)
})