expo-duooomi-app/app/device.tsx

454 lines
16 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 { Ionicons } from '@expo/vector-icons'
import { root } from '@repo/core'
import { FirmwareController } from '@repo/sdk'
import { Block, ConfirmModal, ListEmpty, SyncProgressToast, Text, Toast, VideoBox } from '@share/components'
import { FlashList } from '@shopify/flash-list'
import { router, Stack, useFocusEffect } from 'expo-router'
import { observer } from 'mobx-react-lite'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { ActivityIndicator } from 'react-native'
import { bleManager } from '@/ble/managers/bleManager'
import BannerSection from '@/components/BannerSection'
import { bleStore, userStore } from '@/stores'
import { screenWidth } from '@/utils'
import { buildCdnUrl } from '@/utils/getCDNKey'
// ============ 主组件 ============
const Device = observer(() => {
const { user } = userStore
const { isConnected, sn } = bleStore.state
const { galleryList } = bleStore
const itemWidth = Math.floor((screenWidth - 12 * 2 - 12 * 2) / 3)
// ✅ 本地状态:直接从 Promise 获取数据
const [deviceInfo, setDeviceInfo] = useState<any>(null)
const [version, setVersion] = useState<string>('')
const [memoryStats, setMemoryStats] = useState({
total: 0n,
used: 0n,
available: 0n,
})
const [loading, setLoading] = useState(false)
const [binInfo, setBinInfo] = useState<any>(null)
const loadBin = async () => {
const firmware = root.get(FirmwareController)
const status = process.env.EXPO_PUBLIC_ENV === 'production' ? 'PUBLISHED' : 'DRAFT'
firmware.getLatestPublished('duomi', status).then((r) => {
console.log('latest duomi firmware:', r)
setBinInfo(r)
})
}
useEffect(() => {
console.log('expo env------------', process.env.EXPO_PUBLIC_ENV)
}, [])
// ✅ 页面聚焦时自动获取设备信息和版本号
useFocusEffect(
useCallback(() => {
if (isConnected) {
queryDeviceData()
}
}, [isConnected]),
)
useEffect(() => {
if (!isConnected) {
Toast.show({ title: '设备已断开连接' })
router.back()
}
}, [isConnected])
// ✅ 直接从 Promise 获取数据
const queryDeviceData = useCallback(async () => {
try {
setLoading(true)
loadBin()
// 并行获取设备版本号和设备信息
const [versionInfo, deviceInfoData] = await Promise.all([
bleManager.getDeviceVersion().catch((err) => {
console.warn('[Device] Failed to get version:', err)
return null
}),
bleManager.getDeviceInfo().catch((err) => {
console.warn('[Device] Failed to get device info:', err)
return null
}),
])
console.log('queryDeviceData------------', deviceInfoData)
console.log('versionInfo------------', versionInfo)
// ✅ 直接使用返回的数据更新本地状态
if (versionInfo) {
setVersion(versionInfo.version || '')
}
if (deviceInfoData) {
setDeviceInfo(deviceInfoData)
// 计算内存信息
const totalMemory = deviceInfoData.allspace || 0n
const availableMemory = deviceInfoData.freespace || 0n
const usedMemory = totalMemory - availableMemory
setMemoryStats({
total: totalMemory,
used: usedMemory,
available: availableMemory,
})
}
} catch (error) {
console.error('[Device] Failed to query device data:', error)
Toast.show({ title: '获取设备信息失败' })
} finally {
setLoading(false)
}
}, [])
const handleOtaUpgrade = async () => {
try {
if (!binInfo?.fileUrl) {
Toast?.show({ title: '固件文件地址无效' })
return
}
bleStore.setState((prestate) => {
return { ...prestate, transferProgress: 0 }
})
Toast.showLoading({
renderContent: () => <SyncProgressToast title="固件升级中" />,
duration: 0,
})
const buffer = await bleManager.performOtaUpgrade(binInfo.fileUrl)
Toast.hideLoading()
Toast?.show({ title: `固件升级成功 (${buffer.byteLength} bytes) `, duration: 2e3 })
// 升级成功后重新拉取设备数据
queryDeviceData()
} catch (error) {
console.error('固件升级失败:', error)
const msg = typeof error === 'string' ? error : (error as any)?.message || '固件升级失败'
Toast?.show({ title: msg })
Toast.hideLoading()
} finally {
Toast.hide()
Toast.hideLoading()
}
}
// ✅ 固件更新处理
const handleFirmwareUpdate = useCallback(async () => {
if (!isConnected) {
Toast.show({ title: '请先连接设备' })
return
}
if (!binInfo?.version) {
Toast.show({ title: '无法获取固件信息' })
return
}
Toast.showModal(
<ConfirmModal
title="固件更新"
content={
<Block className="space-y-[8px]">
<Text className="text-[14px] font-bold leading-relaxed text-gray-800"></Text>
<Block className="rounded-lg bg-gray-100 p-[8px]">
<Text className="text-[12px] text-gray-600">{version}</Text>
<Text className="text-[12px] text-green-600">{binInfo?.version}</Text>
</Block>
<Text className="text-[11px] text-gray-500">
; 15 ; APP
</Text>
</Block>
}
confirmText="开始更新"
onCancel={() => Toast.hideModal()}
onConfirm={async () => {
Toast.hideModal()
Toast.show({ title: '开始更新固件...' })
// TODO: 实现固件更新逻辑
handleOtaUpgrade()
}}
/>,
)
}, [isConnected, binInfo, version])
// ✅ 计算内存百分比和显示文本(处理 bigint
const memoryDisplay = useMemo(() => {
if (memoryStats.total === 0n) return null
const totalMB = Number(memoryStats.total) / 1024
const usedMB = Number(memoryStats.used) / 1024
const availableMB = Number(memoryStats.available) / 1024
const usagePercent =
memoryStats.total > 0n ? Math.round((Number(memoryStats.used) / Number(memoryStats.total)) * 100) : 0
return {
total: totalMB.toFixed(1),
used: usedMB.toFixed(1),
available: availableMB.toFixed(1),
percent: usagePercent,
}
}, [memoryStats])
const renderGridItem = useCallback(
({ item }: { item: any }) => {
const handleDeleteConfirm = async () => {
try {
// ✅ 直接使用 Promise 返回值
const result = await bleManager.deleteFile(item)
console.log('result--------------', result)
if (result.success === 0) {
bleStore.removeGalleryItem(item)
Toast.hideModal()
Toast.show({ title: '删除成功' })
} else {
const errorMsg = result.success === 1 ? '删除失败' : result.success === 2 ? '文件不存在' : '未知错误'
Toast.show({ title: errorMsg })
}
} catch (error: any) {
console.error('Error deleting file:', error)
Toast.show({ title: error.message || '删除失败' })
}
}
const handleDelete = async () => {
if (!isConnected) {
Toast.show({ title: '请先连接设备' })
return
}
if (galleryList.length <= 1) {
Toast.show({ title: '至少保留一个文件' })
return
}
Toast.showModal(
<ConfirmModal
title="删除文件"
content={
<Text className="text-[14px] font-bold leading-relaxed text-gray-800"></Text>
}
onCancel={() => Toast.hideModal()}
onConfirm={handleDeleteConfirm}
/>,
)
}
const url = buildCdnUrl(item)
if (!url) return null
return (
<Block className="relative">
<Block
className={`relative overflow-hidden border-2 ${'shadow-hard-black'} ${'border-black'}`}
style={{ transform: [{ skewX: '-6deg' }], height: itemWidth, width: itemWidth }}
>
<Block className="flex items-center justify-center" style={{ height: itemWidth, width: itemWidth }}>
<VideoBox style={{ width: itemWidth, height: itemWidth }} url={url} />
</Block>
{/* 删除按钮 */}
<Block
className="absolute right-1 top-1 z-20 size-6 items-center justify-center rounded-full border-2 border-black bg-[#e61e25] shadow-hard-black"
style={{ transform: [{ skewX: '6deg' }] }}
onClick={() => {
handleDelete()
}}
>
<Ionicons color="white" name="trash-outline" size={12} />
</Block>
</Block>
</Block>
)
},
[itemWidth, isConnected, galleryList.length],
)
const renderHeader = () => (
<Block className="flex-row items-center justify-between px-[16px]" style={{ paddingTop: 12, paddingBottom: 12 }}>
<Block className="ml-[-8px] size-[40px] items-center justify-center" opacity={0.7} onClick={() => router.back()}>
<Ionicons color="white" name="chevron-back" size={24} />
</Block>
<Text className="text-[16px] font-[700] text-white"></Text>
<Block className="w-[32px]" />
</Block>
)
// ✅ 显示设备信息(名称、品牌、分辨率、电池电量、内存等)
const renderDeviceInfo = () => {
return (
<Block className="mx-[16px] mb-[12px] rounded-lg border-2 border-black bg-gray-900 p-[12px] shadow-hard-black">
{/* 标题栏 */}
<Block className="mb-[8px] flex-row items-center justify-between">
<Text className="text-[14px] font-bold text-white"></Text>
{loading && <ActivityIndicator color="#FFE500" size="small" />}
</Block>
{/* 设备基本信息 */}
<Block className="mb-[8px] space-y-[8px] border-b border-gray-700 pb-[8px]">
{/* 设备名称 */}
{deviceInfo?.name && (
<Block className="flex-row items-center justify-between">
<Text className="text-[12px] text-gray-500"></Text>
<Text className="text-[12px] text-white">{deviceInfo.name}</Text>
</Block>
)}
{/* 品牌 */}
{deviceInfo?.brand && (
<Block className="flex-row items-center justify-between">
<Text className="text-[12px] text-gray-500"></Text>
<Text className="text-[12px] font-bold text-white">{deviceInfo.brand}</Text>
</Block>
)}
{/* 分辨率 */}
{deviceInfo?.size && (
<Block className="flex-row items-center justify-between">
<Text className="text-[12px] text-gray-500"></Text>
<Text className="text-[12px] font-bold text-white">{deviceInfo?.size}</Text>
</Block>
)}
{deviceInfo?.powerlevel !== undefined && (
<Block className="flex-row items-center justify-between">
<Text className="text-[12px] text-gray-500"></Text>
<Block className="flex-row items-center gap-[4px]">
<Block
className="h-[4px] w-[20px] rounded-sm bg-gray-600"
style={{
backgroundColor:
deviceInfo.powerlevel > 50 ? '#22c55e' : deviceInfo.powerlevel > 20 ? '#eab308' : '#ef4444',
}}
/>
<Text className="text-[12px] font-bold text-white">{deviceInfo.powerlevel}%</Text>
</Block>
</Block>
)}
{/* 设备SN码 */}
{sn && (
<Block className="flex-row items-center justify-between">
<Text className="text-[12px] text-gray-500">SN码</Text>
<Text className="text-[12px] font-bold text-white">{sn}</Text>
</Block>
)}
{/* 固件版本 */}
{version && (
<Block className="flex-row items-center justify-between">
<Text className="text-[12px] text-gray-500"></Text>
<Text className="text-[12px] font-bold text-white">{version}</Text>
</Block>
)}
{/* 最新固件版本 */}
{binInfo?.version && (
<Block className="flex-row items-center justify-between">
<Text className="text-[12px] text-gray-500"></Text>
<Text className="text-[12px] font-bold text-green-400">{binInfo.version}</Text>
</Block>
)}
{/* 电池电量 */}
</Block>
{/* 固件更新按钮 */}
{binInfo?.version && version && binInfo.version !== version && (
<Block className="mt-[12px]">
<Block
className="flex-row items-center justify-between rounded-lg border-2 border-yellow-400 bg-yellow-400/10 px-[12px] py-[8px]"
onClick={handleFirmwareUpdate}
>
<Block className="flex-1">
<Text className="text-[12px] font-bold text-yellow-400"></Text>
<Text className="mt-[2px] text-[10px] text-gray-400">
{version} {binInfo.version}
</Text>
</Block>
<Block className="flex-row items-center gap-[4px] rounded bg-yellow-400 px-[12px] py-[6px]">
<Ionicons color="black" name="download-outline" size={14} />
<Text className="text-[12px] font-bold text-black"></Text>
</Block>
</Block>
</Block>
)}
{/* 内存信息 */}
{memoryDisplay && (
<Block className="mt-[12px] space-y-[8px]">
{/* 内存标题 */}
<Block className="mb-[8px] flex-row items-center justify-between">
<Text className="text-[12px] font-bold text-white"></Text>
<Text className="text-[11px] text-gray-400">{memoryDisplay.percent}% 使</Text>
</Block>
{/* 内存进度条 */}
<Block className="h-[6px] overflow-hidden rounded-full bg-gray-700">
<Block
className="h-full bg-yellow-400 bg-gradient-to-r"
style={{
height: 6,
width: `${memoryDisplay.percent}%`,
}}
/>
</Block>
{/* 内存详细信息 */}
<Block className="mt-[8px] flex-row justify-between gap-[8px]">
<Block className="flex-1">
<Text className="mb-[2px] text-[10px] text-gray-500">使</Text>
<Text className="text-[12px] font-bold text-yellow-400">{memoryDisplay.used} MB</Text>
</Block>
<Block className="flex-1">
<Text className="mb-[2px] text-[10px] text-gray-500"></Text>
<Text className="text-[12px] font-bold text-green-400">{memoryDisplay.available} MB</Text>
</Block>
<Block className="flex-1">
<Text className="mb-[2px] text-[10px] text-gray-500"></Text>
<Text className="text-[12px] font-bold text-white">{memoryDisplay.total} MB</Text>
</Block>
</Block>
</Block>
)}
</Block>
)
}
return (
<Block className="relative flex-1 bg-black">
<BannerSection />
<Stack.Screen options={{ headerShown: false }} />
{renderHeader()}
<Block className="z-10 flex-1">
{renderDeviceInfo()}
<FlashList
contentContainerStyle={{ paddingHorizontal: 12, paddingBottom: 200 }}
data={galleryList}
drawDistance={1200}
// @ts-ignore
estimatedItemSize={itemWidth}
getItemType={() => 'row'}
ItemSeparatorComponent={() => <Block style={{ height: 6 }} />}
keyExtractor={(item) => item}
ListEmptyComponent={<ListEmpty />}
numColumns={3}
renderItem={renderGridItem}
/>
</Block>
</Block>
)
})
export default Device