import * as Sentry from '@sentry/react-native' import { type Directory, Paths } from 'expo-file-system' import * as FileSystem from 'expo-file-system/legacy' import * as ImagePicker from 'expo-image-picker' import * as MediaLibrary from 'expo-media-library' import { observer } from 'mobx-react-lite' import React, { useState } from 'react' import { Button, StyleSheet, TextInput } from 'react-native' import { KeyboardAwareScrollView } from 'react-native-keyboard-controller' import { imgPicker } from '@/@share/apis' import { Block, Toast, VideoBox } from '@/@share/components' import { APP_VERSION } from '@/app.constants' import { BLE_UUIDS, PROTOCOL_VERSION } from '@/ble' import { bleManager } from '@/ble/managers/bleManager' import { ThemedText } from '@/components/themed-text' import { ThemedView } from '@/components/themed-view' import { bleStore, userStore } from '@/stores' import { uploadFile } from '@/utils' import { buildCdnUrl } from '@/utils/getCDNKey' export default observer(function TabTwoScreen() { const { user } = userStore const { isScanning, isConnected, connectedDevice, deviceInfo, version, isActivated, transferProgress, discoveredDevices, loading, error, contents, } = bleStore.state const bleSessionId = bleStore.bleSessionId const startScan = () => bleManager.startScan() const stopScan = () => bleManager.stopScan() const connectToDevice = (device: any) => bleManager.connectToDevice(device) const disconnectDevice = () => bleManager.disconnectDevice() const getDeviceInfo = () => bleManager.getDeviceInfo() const getDeviceVersion = () => bleManager.getDeviceVersion() const bindDevice = (userId: string) => bleManager.bindDevice(userId) const unBindDevice = (userId: string) => bleManager.unBindDevice(userId) const transferMediaSingle = (url: string) => bleManager.transferMediaSingle(url) const deleteFile = (fileName: string) => bleManager.deleteFile(fileName) const [imageUri, setImageUri] = useState( 'file:///data/user/0/com.bowong.duooomi/cache/ImageManipulator/153903d9-f5e9-4e65-9c7d-f7bedd574f3c.jpg', ) const [userId, setUserId] = useState(user?.id ?? '01duHavK1CMW7pawcgOtB5aUqQeHPeni') // 查询设备版本 const queryDeviceVersion = async () => { try { await getDeviceVersion() Toast?.show({ title: '版本查询请求已发送' }) } catch (error) { console.error('Error querying version:', error) Toast?.show({ title: '版本查询失败' }) } } // 请求设备信息 const requestDeviceInfo = async () => { try { await getDeviceInfo() Toast?.show({ title: '设备信息查询请求已发送' }) } catch (error) { console.error('Error requesting device info:', error) Toast?.show({ title: `设备信息查询失败, ${error}` }) } } // 查询激活状态(通过获取设备信息来判断) const queryActivationStatus = async () => { try { await getDeviceInfo() Toast?.show({ title: '激活状态查询请求已发送' }) } catch (error) { console.error('Error querying activation:', error) Toast?.show({ title: `激活状态查询失败, ${error}` }) } } // 绑定设备 const handleBindDevice = async () => { try { await bindDevice(userId) Toast?.show({ title: '设备绑定请求已发送' }) } catch (error) { console.error('Error binding device:', error) Toast?.show({ title: `设备绑定失败, ${error}` }) } } // 解绑设备 const handleUnbindDevice = async () => { try { await unBindDevice(userId) Toast?.show({ title: '设备解绑请求已发送' }) } catch (error) { console.error('Error unbinding device:', error) Toast?.show({ title: `设备解绑失败, ${error}` }) } } const convertToANIAsBuffer = async ( tempDir: Directory, media: ImagePicker.ImagePickerAsset, ): Promise => { try { const formData = new FormData() formData.append('file', { uri: media.uri, name: media.fileName || 'video.mp4', type: media.mimeType || 'video/mp4', } as any) const aniTest = `https://bowongai-test--ani-video-converter-fastapi-app.modal.run/api/convert/ani` const aniProd = `https://bowongai-prod--ani-video-converter-fastapi-app.modal.run/api/convert/ani` const response = await fetch(aniProd, { method: 'POST', body: formData, headers: { Accept: 'multipart/form-data' }, }) if (!response.ok) { throw new Error(`Conversion failed with status ${response.status}`) } const content = await response.arrayBuffer() console.debug(`Converted video size : ${content.byteLength} bytes`) return content } catch (error) { console.error('Error converting video to ANI:', error) throw error } } const handleUploadMedia = async () => { try { const assetList = await imgPicker({ maxImages: 1, type: ImagePicker.MediaTypeOptions.All, resultType: 'asset' }) const asset = assetList[0] as ImagePicker.ImagePickerAsset if (!asset) { console.warn('No asset selected') return } // return if (asset?.uri) { Toast.showLoading({ title: '文件传输中...', duration: 0 }) setImageUri(asset.uri) const url = await uploadFile({ uri: asset.uri, mimeType: asset.mimeType, fileName: asset.fileName ?? undefined, }) transferMediaSingle(url) .then(() => { Toast?.show({ title: '文件传输完成' }) }) .catch((error) => { const { status } = error || {} if (!status) { Toast?.show({ title: `文件传输失败` }) return } if (status === 'no_space') { Toast?.show({ title: '文件传输失败, 设备存储空间不足' }) } if (status === 'duplicated') { Toast?.show({ title: '文件传输失败, 设备已存在相同文件' }) } }) .finally(() => { Toast.hideLoading() }) } } catch (error) { console.error('Error uploading media:', error) } } const downloadFile = async () => { try { const perm = await MediaLibrary.requestPermissionsAsync() if (perm.status !== 'granted') { Toast?.show({ title: '请开启相册权限' }) return } const filename = assetFileName || 'image.jpg' let localUri = imageUri if (!localUri.startsWith('file://')) { const target = `${Paths.documentDirectory}${filename}` const res = await FileSystem.downloadAsync(imageUri, target) localUri = res.uri } await MediaLibrary.saveToLibraryAsync(localUri) console.log('已保存到系统相册:', localUri) Toast?.show({ title: '保存成功' }) } catch (error) { console.error('保存失败:', error) Toast?.show({ title: '保存失败' }) } } const renderImage = () => { if (!imageUri) { return null } return ( ) } return ( BLE Explorer 插件当前版本号: {APP_VERSION} Session ID: {bleSessionId}