feat: 更新 APP 版本号,优化文件传输逻辑,添加 CDN key 提取功能
This commit is contained in:
parent
0e647b033e
commit
50b401b342
|
|
@ -8,6 +8,6 @@ export const ANDROID_ID = 'com.duomi.duooomi'
|
||||||
export const IOS_ID = ANDROID_ID
|
export const IOS_ID = ANDROID_ID
|
||||||
export const IOS_UNIVERSAL_LINK = 'duooomi.bowong.cn'
|
export const IOS_UNIVERSAL_LINK = 'duooomi.bowong.cn'
|
||||||
export const OWNER_ID = 'x3xbTCWf7dbtWu4gGU2TeI054L77xtkt'
|
export const OWNER_ID = 'x3xbTCWf7dbtWu4gGU2TeI054L77xtkt'
|
||||||
export const APP_VERSION = 'dev202512311544'
|
export const APP_VERSION = 'dev202601071100'
|
||||||
export const ALIPAY_SCHEMA = 'alipay2021006119657394'
|
export const ALIPAY_SCHEMA = 'alipay2021006119657394'
|
||||||
export const ALIPAY_SCHEMA_SANDBOX = '9021000158673972'
|
export const ALIPAY_SCHEMA_SANDBOX = '9021000158673972'
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import * as FileSystem from 'expo-file-system/legacy'
|
||||||
import { Image } from 'expo-image'
|
import { Image } from 'expo-image'
|
||||||
import * as ImagePicker from 'expo-image-picker'
|
import * as ImagePicker from 'expo-image-picker'
|
||||||
import * as MediaLibrary from 'expo-media-library'
|
import * as MediaLibrary from 'expo-media-library'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Button, StyleSheet, TextInput } from 'react-native'
|
import { Button, StyleSheet, TextInput } from 'react-native'
|
||||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'
|
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'
|
||||||
|
|
@ -14,9 +15,12 @@ import { APP_VERSION } from '@/app.constants'
|
||||||
import { BLE_UUIDS, PROTOCOL_VERSION, useBleExplorer } from '@/ble'
|
import { BLE_UUIDS, PROTOCOL_VERSION, useBleExplorer } from '@/ble'
|
||||||
import { ThemedText } from '@/components/themed-text'
|
import { ThemedText } from '@/components/themed-text'
|
||||||
import { ThemedView } from '@/components/themed-view'
|
import { ThemedView } from '@/components/themed-view'
|
||||||
|
import { userStore } from '@/stores'
|
||||||
import { uploadFile } from '@/utils'
|
import { uploadFile } from '@/utils'
|
||||||
|
import { buildCdnUrl } from '@/utils/getCDNKey'
|
||||||
|
|
||||||
export default function TabTwoScreen() {
|
export default observer(function TabTwoScreen() {
|
||||||
|
const { user } = userStore
|
||||||
const {
|
const {
|
||||||
isScanning,
|
isScanning,
|
||||||
isConnected,
|
isConnected,
|
||||||
|
|
@ -45,7 +49,7 @@ export default function TabTwoScreen() {
|
||||||
const [imageUri, setImageUri] = useState(
|
const [imageUri, setImageUri] = useState(
|
||||||
'file:///data/user/0/com.bowong.duooomi/cache/ImageManipulator/153903d9-f5e9-4e65-9c7d-f7bedd574f3c.jpg',
|
'file:///data/user/0/com.bowong.duooomi/cache/ImageManipulator/153903d9-f5e9-4e65-9c7d-f7bedd574f3c.jpg',
|
||||||
)
|
)
|
||||||
const [userId, setUserId] = useState('01duHavK1CMW7pawcgOtB5aUqQeHPeni')
|
const [userId, setUserId] = useState(user?.id ?? '01duHavK1CMW7pawcgOtB5aUqQeHPeni')
|
||||||
|
|
||||||
// 查询设备版本
|
// 查询设备版本
|
||||||
const queryDeviceVersion = async () => {
|
const queryDeviceVersion = async () => {
|
||||||
|
|
@ -137,7 +141,11 @@ export default function TabTwoScreen() {
|
||||||
try {
|
try {
|
||||||
const assetList = await imgPicker({ maxImages: 1, type: ImagePicker.MediaTypeOptions.All, resultType: 'asset' })
|
const assetList = await imgPicker({ maxImages: 1, type: ImagePicker.MediaTypeOptions.All, resultType: 'asset' })
|
||||||
|
|
||||||
const asset = assetList[0]
|
const asset = assetList[0] as ImagePicker.ImagePickerAsset
|
||||||
|
if (!asset) {
|
||||||
|
console.warn('No asset selected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// return
|
// return
|
||||||
if (asset?.uri) {
|
if (asset?.uri) {
|
||||||
|
|
@ -147,7 +155,7 @@ export default function TabTwoScreen() {
|
||||||
const url = await uploadFile({
|
const url = await uploadFile({
|
||||||
uri: asset.uri,
|
uri: asset.uri,
|
||||||
mimeType: asset.mimeType,
|
mimeType: asset.mimeType,
|
||||||
fileName: asset.fileName,
|
fileName: asset.fileName ?? undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
transferMediaSingle(url)
|
transferMediaSingle(url)
|
||||||
|
|
@ -155,15 +163,18 @@ export default function TabTwoScreen() {
|
||||||
Toast?.show({ title: '文件传输完成' })
|
Toast?.show({ title: '文件传输完成' })
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error === 'no_space') {
|
const { status } = error || {}
|
||||||
|
if (!status) {
|
||||||
|
Toast?.show({ title: `文件传输失败` })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'no_space') {
|
||||||
Toast?.show({ title: '文件传输失败, 设备存储空间不足' })
|
Toast?.show({ title: '文件传输失败, 设备存储空间不足' })
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if (error === 'duplicated') {
|
if (status === 'duplicated') {
|
||||||
Toast?.show({ title: '文件传输失败, 设备已存在相同文件' })
|
Toast?.show({ title: '文件传输失败, 设备已存在相同文件' })
|
||||||
return
|
|
||||||
}
|
}
|
||||||
Toast?.show({ title: `文件传输失败,${error}` })
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
Toast.hideLoading()
|
Toast.hideLoading()
|
||||||
|
|
@ -370,14 +381,15 @@ export default function TabTwoScreen() {
|
||||||
// .fill('https://cdn.roasmax.cn/material/8836b879f2d44af48eef8da82d13a755.mp4')
|
// .fill('https://cdn.roasmax.cn/material/8836b879f2d44af48eef8da82d13a755.mp4')
|
||||||
contents.map((item, index) => {
|
contents.map((item, index) => {
|
||||||
console.log('item-----------', item)
|
console.log('item-----------', item)
|
||||||
|
const url = buildCdnUrl(item)
|
||||||
|
if (!url) return null
|
||||||
const Width = 100
|
const Width = 100
|
||||||
return (
|
return (
|
||||||
<Block key={`${item}-${index}`} className="gap-2">
|
<Block key={`${item}-${index}`} className="gap-2">
|
||||||
<ThemedText style={styles.contentText} numberOfLines={1}>
|
<ThemedText style={styles.contentText} numberOfLines={1}>
|
||||||
{item}
|
{item}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<VideoBox style={{ width: Width, height: Width }} url={item} />
|
<VideoBox style={{ width: Width, height: Width }} url={url} />
|
||||||
<Button
|
<Button
|
||||||
title="删除"
|
title="删除"
|
||||||
color="red"
|
color="red"
|
||||||
|
|
@ -410,7 +422,7 @@ export default function TabTwoScreen() {
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
</KeyboardAwareScrollView>
|
</KeyboardAwareScrollView>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
headerImage: {
|
headerImage: {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Alert, Linking, PermissionsAndroid, Platform } from 'react-native'
|
||||||
import { ScanMode } from 'react-native-ble-plx'
|
import { ScanMode } from 'react-native-ble-plx'
|
||||||
|
|
||||||
import { aniStorage } from '@/utils/aniStorage'
|
import { aniStorage } from '@/utils/aniStorage'
|
||||||
|
import { extractCdnKey } from '@/utils/getCDNKey'
|
||||||
|
|
||||||
import { DeviceInfoService } from '../services/DeviceInfoService'
|
import { DeviceInfoService } from '../services/DeviceInfoService'
|
||||||
import { FileTransferService } from '../services/FileTransferService'
|
import { FileTransferService } from '../services/FileTransferService'
|
||||||
|
|
@ -52,6 +53,16 @@ export const useBleExplorer = () => {
|
||||||
// 用于暂存扫描到的设备,避免频繁更新状态
|
// 用于暂存扫描到的设备,避免频繁更新状态
|
||||||
const pendingDevicesRef = useRef<BleDevice[]>([])
|
const pendingDevicesRef = useRef<BleDevice[]>([])
|
||||||
const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
const pendingPrepareTransfersRef = useRef<
|
||||||
|
Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
resolve: (value: PrepareTransferResponse) => void
|
||||||
|
reject: (reason?: any) => void
|
||||||
|
timeoutId: ReturnType<typeof setTimeout>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>(new Map())
|
||||||
|
|
||||||
const [state, setState2] = useState<BleState>({
|
const [state, setState2] = useState<BleState>({
|
||||||
isScanning: false,
|
isScanning: false,
|
||||||
|
|
@ -312,7 +323,17 @@ export const useBleExplorer = () => {
|
||||||
|
|
||||||
const onPrepareTransfer = (response: PrepareTransferResponse) => {
|
const onPrepareTransfer = (response: PrepareTransferResponse) => {
|
||||||
console.log('Prepare transfer response:', response)
|
console.log('Prepare transfer response:', response)
|
||||||
// 可以在这里根据状态做相应处理
|
|
||||||
|
// 检查是否有对应的待处理 Promise
|
||||||
|
const pending = pendingPrepareTransfersRef.current.get(response.key)
|
||||||
|
if (pending) {
|
||||||
|
clearTimeout(pending.timeoutId)
|
||||||
|
pendingPrepareTransfersRef.current.delete(response.key)
|
||||||
|
pending.resolve(response)
|
||||||
|
return // 早期返回,避免继续处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有待处理 Promise,才做其他处理
|
||||||
if (response.status === 'ready') {
|
if (response.status === 'ready') {
|
||||||
console.log('Device is ready to receive file:', response.key)
|
console.log('Device is ready to receive file:', response.key)
|
||||||
} else if (response.status === 'no_space') {
|
} else if (response.status === 'no_space') {
|
||||||
|
|
@ -581,29 +602,19 @@ export const useBleExplorer = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建一个 Promise 来等待响应
|
// 创建 Promise 并存储到 Map
|
||||||
const responsePromise = new Promise<PrepareTransferResponse>((resolve, reject) => {
|
const responsePromise = new Promise<PrepareTransferResponse>((resolve, reject) => {
|
||||||
let timeoutId: ReturnType<typeof setTimeout>
|
const timeoutId = setTimeout(() => {
|
||||||
|
pendingPrepareTransfersRef.current.delete(key)
|
||||||
const handleResponse = (response: PrepareTransferResponse) => {
|
|
||||||
if (response.key === key) {
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
deviceInfoService.removeListener(EVENT_TYPES.PREPARE_TRANSFER.name, handleResponse)
|
|
||||||
resolve(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加临时监听器
|
|
||||||
deviceInfoService.addListener(EVENT_TYPES.PREPARE_TRANSFER.name, handleResponse)
|
|
||||||
|
|
||||||
// 设置超时
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
deviceInfoService.removeListener(EVENT_TYPES.PREPARE_TRANSFER.name, handleResponse)
|
|
||||||
reject(new Error('Prepare transfer timeout'))
|
reject(new Error('Prepare transfer timeout'))
|
||||||
}, 10000) // 10秒超时
|
}, 10000) // 10秒超时
|
||||||
|
|
||||||
|
// 先注册 Promise,再发送请求
|
||||||
|
pendingPrepareTransfersRef.current.set(key, { resolve, reject, timeoutId })
|
||||||
})
|
})
|
||||||
|
|
||||||
// 发送预先发送请求
|
// ✅ 关键:先注册 Promise,再发送请求
|
||||||
|
// 这样即使设备立即返回,Promise 也已经准备好了
|
||||||
await deviceInfoService.prepareTransfer(state.connectedDevice.id, key, size)
|
await deviceInfoService.prepareTransfer(state.connectedDevice.id, key, size)
|
||||||
|
|
||||||
// 等待响应
|
// 等待响应
|
||||||
|
|
@ -754,7 +765,7 @@ export const useBleExplorer = () => {
|
||||||
let imageUri = media.uri
|
let imageUri = media.uri
|
||||||
|
|
||||||
// Check if conversion is needed (if not jpeg)
|
// Check if conversion is needed (if not jpeg)
|
||||||
// Note: mimeType might not always be reliable, so we can also check filename extension if needed,
|
// Note: mimeType might not always be reliable, so we can also check filename extension if needed,
|
||||||
// or just always run manipulator to ensure consistency.
|
// or just always run manipulator to ensure consistency.
|
||||||
// Here we check if it is NOT jpeg/jpg
|
// Here we check if it is NOT jpeg/jpg
|
||||||
const isJpeg =
|
const isJpeg =
|
||||||
|
|
@ -927,11 +938,16 @@ export const useBleExplorer = () => {
|
||||||
|
|
||||||
const fileSizeByte = tempBuffer.byteLength
|
const fileSizeByte = tempBuffer.byteLength
|
||||||
|
|
||||||
const prepareResp = await prepareTransfer(uriOrUrl, fileSizeByte)
|
const key = extractCdnKey(uriOrUrl)
|
||||||
|
if (!key) {
|
||||||
|
setState((prev) => ({ ...prev, loading: { ...prev.loading, converting: false } }))
|
||||||
|
return Promise.reject('Invalid uriOrUrl key')
|
||||||
|
}
|
||||||
|
const prepareResp = await prepareTransfer(key, fileSizeByte)
|
||||||
if (prepareResp.status !== 'ready') {
|
if (prepareResp.status !== 'ready') {
|
||||||
setState((prev) => ({ ...prev, loading: { ...prev.loading, converting: false } }))
|
setState((prev) => ({ ...prev, loading: { ...prev.loading, converting: false } }))
|
||||||
console.log('prepareResp not ready-----', prepareResp)
|
console.log('prepareResp not ready-----', prepareResp)
|
||||||
return Promise.reject(`${prepareResp.status}`)
|
return Promise.reject({ status: `${prepareResp.status}` })
|
||||||
}
|
}
|
||||||
console.log('prepareResp-----', prepareResp)
|
console.log('prepareResp-----', prepareResp)
|
||||||
|
|
||||||
|
|
@ -955,7 +971,7 @@ export const useBleExplorer = () => {
|
||||||
return Promise.reject(error.message)
|
return Promise.reject(error.message)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[state.connectedDevice, fileTransferService, convertToANIAsBuffer, setError],
|
[state.connectedDevice, fileTransferService, convertImgToANIAsBuffer, setError],
|
||||||
)
|
)
|
||||||
|
|
||||||
const clearLogs = useCallback(() => setState((prev) => ({ ...prev, logs: [] })), [])
|
const clearLogs = useCallback(() => setState((prev) => ({ ...prev, logs: [] })), [])
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ interface UseUpdateCheckerOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpdateChecker = ({
|
export const useUpdateChecker = ({
|
||||||
interval = 1 * 30 * 1000, // 30秒
|
interval = 5 * 60 * 1000, // 5分钟
|
||||||
enablePeriodicCheck = true,
|
enablePeriodicCheck = true,
|
||||||
}: UseUpdateCheckerOptions = {}) => {
|
}: UseUpdateCheckerOptions = {}) => {
|
||||||
const [hasUpdate, setHasUpdate] = useState(false)
|
const [hasUpdate, setHasUpdate] = useState(false)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { CNDHOST } from './../app.config'
|
||||||
|
const ensureTrailingSlash = (s: string) => (s.endsWith('/') ? s : s + '/')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 CDN 地址中提取 key(例如返回 material/xxx.mp4)
|
||||||
|
* @param input 完整URL,如 https://cdn.roasmax.cn/material/xxx.mp4?x=y
|
||||||
|
* @param cdnHost 可选,默认使用 https://cdn.roasmax.cn/
|
||||||
|
*/
|
||||||
|
export const extractCdnKey = (input: string, cdnHost = CNDHOST): string | null => {
|
||||||
|
if (!input) return null
|
||||||
|
|
||||||
|
const base = ensureTrailingSlash(cdnHost)
|
||||||
|
|
||||||
|
// 简单前缀匹配(最快)
|
||||||
|
if (input.startsWith(base)) {
|
||||||
|
const rest = input.slice(base.length)
|
||||||
|
return rest.split(/[?#]/)[0].replace(/^\/+/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL 解析兜底
|
||||||
|
try {
|
||||||
|
const u = new URL(input)
|
||||||
|
const b = new URL(base)
|
||||||
|
if (u.origin === b.origin) {
|
||||||
|
return u.pathname.replace(/^\/+/, '')
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch {
|
||||||
|
// 不是URL,直接返回 null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断是否为指定 CDN 的地址 */
|
||||||
|
export const isFromCdn = (input: string, cdnHost = CNDHOST): boolean => {
|
||||||
|
if (!input) return false
|
||||||
|
try {
|
||||||
|
const u = new URL(input)
|
||||||
|
const b = new URL(ensureTrailingSlash(cdnHost))
|
||||||
|
return u.origin === b.origin
|
||||||
|
} catch {
|
||||||
|
return input.startsWith(ensureTrailingSlash(cdnHost))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 key 拼接为完整 CDN URL。若传入已是完整URL则直接返回规范化结果
|
||||||
|
* @param keyOrUrl 资源 key 或 完整 URL
|
||||||
|
* @param cdnHost CDN 根域名
|
||||||
|
*/
|
||||||
|
export const buildCdnUrl = (keyOrUrl: string, cdnHost = CNDHOST): string | null => {
|
||||||
|
if (!keyOrUrl) return null
|
||||||
|
const base = ensureTrailingSlash(cdnHost)
|
||||||
|
|
||||||
|
// 已是该 CDN 的完整 URL,返回去除查询/哈希后的规范化结果
|
||||||
|
if (isFromCdn(keyOrUrl, cdnHost)) {
|
||||||
|
try {
|
||||||
|
const u = new URL(keyOrUrl)
|
||||||
|
return `${u.origin}${u.pathname}`
|
||||||
|
} catch {
|
||||||
|
return keyOrUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 传入为其他域的URL或纯key,提取路径作为key再拼接
|
||||||
|
const pathOrKey = (() => {
|
||||||
|
try {
|
||||||
|
const u = new URL(keyOrUrl)
|
||||||
|
return u.pathname
|
||||||
|
} catch {
|
||||||
|
return keyOrUrl
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
const cleaned = pathOrKey.split(/[?#]/)[0].replace(/^\/+/, '')
|
||||||
|
return base + cleaned
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue