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

436 lines
14 KiB
TypeScript

import React, { useState } from 'react'
import { StyleSheet, Button, TextInput } from 'react-native'
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { BLE_UUIDS, PROTOCOL_VERSION, useBleExplorer } from '@/ble'
import ParallaxScrollView from '@/components/parallax-scroll-view'
import { IconSymbol } from '@/components/ui/icon-symbol'
import { imgPicker } from '@/@share/apis'
import * as ImagePicker from 'expo-image-picker'
import * as ImageManipulator from 'expo-image-manipulator'
import { File, Directory, Paths } from 'expo-file-system'
import * as FileSystem from 'expo-file-system'
import * as MediaLibrary from 'expo-media-library'
import { Block, Toast } from '@/@share/components'
import { Image } from 'expo-image'
import { APP_VERSION } from '@/app.constants'
import * as Sentry from '@sentry/react-native'
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'
export default function TabTwoScreen() {
const {
isScanning,
isConnected,
connectedDevice,
deviceInfo,
version,
isActivated,
transferProgress,
discoveredDevices,
loading,
error,
startScan,
stopScan,
connectToDevice,
disconnectDevice,
getDeviceInfo,
getDeviceVersion,
bindDevice,
unBindDevice,
transferMediaSingle,
bleSeesionId,
} = useBleExplorer()
const [imageUri, setImageUri] = useState('')
const [assetFileName, setAssetFileName] = useState('image.jpg')
const [userId, setUserId] = useState('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<ArrayBuffer> => {
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.Images,
resultType: 'asset',
})) as unknown as ImagePicker.ImagePickerAsset[]
const asset = assetList[0]
setAssetFileName(asset.fileName || 'image.jpg')
// return
if (asset.uri) {
Toast.showLoading({ title: '文件传输中...', duration: 0 })
setImageUri(asset.uri)
transferMediaSingle(asset.uri)
.then(() => {
Toast?.show({ title: '文件传输完成' })
})
.catch((error) => {
Toast?.show({ title: `文件传输失败,${error}` })
})
.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 (
<Block style={{ width: BLE_UUIDS.SCREEN_SIZE, height: BLE_UUIDS.SCREEN_SIZE }} onClick={downloadFile}>
<Image source={{ uri: imageUri }} contentFit="contain" style={{ width: BLE_UUIDS.SCREEN_SIZE, height: BLE_UUIDS.SCREEN_SIZE }} />
</Block>
)
}
return (
<KeyboardAwareScrollView bottomOffset={100} contentContainerStyle={{ paddingHorizontal: 20, paddingTop: 40, backgroundColor: 'white' }}>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">BLE Explorer</ThemedText>
<ThemedText>: {APP_VERSION}</ThemedText>
<ThemedText>Session ID: {bleSeesionId}</ThemedText>
<TextInput style={styles.input} value={bleSeesionId} placeholder="" placeholderTextColor="#999" editable={true} />
</ThemedView>
<Button
title="Try!"
onPress={() => {
Sentry.captureMessage('Test from Android Build')
// throw new Error('Test Sentry Integration')
}}
/>
{/* Connection Status */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Connection Status</ThemedText>
<ThemedText>Scanning: {isScanning ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Connected: {isConnected ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Device: {connectedDevice?.name || 'None'}</ThemedText>
{error && <ThemedText style={styles.errorText}>Error: {error}</ThemedText>}
</ThemedView>
{/* Discovered Devices */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Discovered Devices</ThemedText>
<ThemedText style={styles.deviceCount}>Total devices found: {discoveredDevices.length}</ThemedText>
{discoveredDevices.length === 0 ? (
<ThemedText>No devices discovered yet. Start scanning to find devices.</ThemedText>
) : (
<ThemedView style={{ gap: 8 }}>
{discoveredDevices.map((item: any) => (
<ThemedView key={item.id} style={[styles.deviceItem, item.connected && styles.connectedDevice]} lightColor="#eee" darkColor="#2a2a2a">
<ThemedView style={styles.deviceInfo} lightColor="transparent" darkColor="transparent">
<ThemedText style={item.connected && styles.connectedDeviceText}>{item.name || 'Unknown Device'}</ThemedText>
<ThemedText style={styles.deviceId}>{item.id}</ThemedText>
{item.serviceUUIDs && item.serviceUUIDs.length > 0 && (
<ThemedText style={styles.serviceUuids}>Services: {item.serviceUUIDs.join(', ')}</ThemedText>
)}
{item.connected && <ThemedText style={styles.connectionStatus}>Connected</ThemedText>}
</ThemedView>
<Button
title={loading.connecting ? 'Connecting...' : item.connected ? 'Connected' : 'Connect'}
onPress={() => connectToDevice(item)}
disabled={isConnected || loading.connecting || item.connected}
/>
</ThemedView>
))}
</ThemedView>
)}
</ThemedView>
{/* Device Info */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Device Information</ThemedText>
<ThemedText>Activated: {isActivated ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Version: {version || 'Unknown'}</ThemedText>
{deviceInfo && (
<>
<ThemedText>Name: {deviceInfo.name}</ThemedText>
<ThemedText>Total Space: {deviceInfo.allspace} KB</ThemedText>
<ThemedText>Free Space: {deviceInfo.freespace} KB</ThemedText>
<ThemedText>Brand: {deviceInfo.brand}</ThemedText>
</>
)}
</ThemedView>
{/* Transfer Status */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">File Transfer</ThemedText>
<ThemedText>Converting: {loading.converting ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Transferring: {loading.transferring ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Progress: {transferProgress}%</ThemedText>
</ThemedView>
{renderImage()}
{/* Control Buttons */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Controls</ThemedText>
<ThemedView style={styles.buttonRow}>
<Button title={isScanning ? '停止扫描' : '开始扫描'} onPress={isScanning ? stopScan : startScan} />
<Button title="断开连接" onPress={disconnectDevice} disabled={!isConnected} />
</ThemedView>
<ThemedView style={styles.buttonRow}>
<Button title={loading.querying ? '获取中...' : '获取版本号'} onPress={queryDeviceVersion} disabled={!isConnected} />
<Button title={loading.querying ? '获取中...' : '获取设备信息'} onPress={requestDeviceInfo} disabled={!isConnected} />
</ThemedView>
<ThemedView style={styles.buttonRow}>
<Button
title={loading.transferring ? '传输中...' : '发文件到设备'}
onPress={handleUploadMedia}
disabled={!isConnected || loading.transferring}
/>
</ThemedView>
<ThemedView style={{ marginTop: 8 }}>
<ThemedText style={{ marginBottom: 4 }}>User ID:</ThemedText>
<TextInput
style={styles.input}
value={userId}
onChangeText={setUserId}
placeholder="输入用户ID"
placeholderTextColor="#999"
// editable={isConnected}
/>
</ThemedView>
<ThemedView style={styles.buttonRow}>
<Button title="绑定设备" onPress={handleBindDevice} disabled={!isConnected || isActivated || !userId.trim()} />
<Button title="解绑设备" onPress={handleUnbindDevice} disabled={!isConnected || !isActivated || !userId.trim()} />
</ThemedView>
</ThemedView>
{/* Protocol Info - Available in both modes */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Protocol Information</ThemedText>
<ThemedText>Version: {PROTOCOL_VERSION}</ThemedText>
<ThemedText>Service UUID: {BLE_UUIDS.SERVICE}</ThemedText>
<ThemedText>Write UUID: {BLE_UUIDS.WRITE_CHARACTERISTIC}</ThemedText>
<ThemedText>Read UUID: {BLE_UUIDS.READ_CHARACTERISTIC}</ThemedText>
</ThemedView>
</KeyboardAwareScrollView>
)
}
const styles = StyleSheet.create({
headerImage: {
color: '#808080',
bottom: -90,
left: -35,
position: 'absolute',
},
titleContainer: {
flexDirection: 'column',
gap: 8,
},
section: {
gap: 8,
marginBottom: 8,
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
gap: 8,
},
logHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
logContainer: {
maxHeight: 200,
padding: 8,
borderRadius: 4,
},
logText: {
fontSize: 12,
marginBottom: 2,
},
deviceItem: {
padding: 12,
marginBottom: 8,
borderRadius: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
deviceInfo: {
flex: 1,
marginRight: 12,
},
deviceId: {
fontSize: 12,
opacity: 0.7,
},
deviceCount: {
fontSize: 14,
opacity: 0.8,
marginTop: 4,
marginBottom: 8,
fontWeight: '500',
},
serviceUuids: {
fontSize: 11,
opacity: 0.6,
marginTop: 2,
fontStyle: 'italic',
},
connectionStatus: {
fontSize: 12,
fontWeight: 'bold',
marginTop: 4,
},
connectedDevice: {
borderWidth: 1,
borderColor: '#4CAF50',
},
connectedDeviceText: {
fontWeight: 'bold',
},
errorText: {
marginTop: 8,
fontWeight: 'bold',
},
modeSwitchContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginVertical: 12,
},
modeLabel: {
fontSize: 16,
marginHorizontal: 8,
},
modeDescription: {
fontSize: 12,
opacity: 0.7,
marginTop: 8,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 4,
padding: 8,
fontSize: 14,
backgroundColor: '#fff',
color: '#000',
},
})