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_UNIVERSAL_LINK = 'duooomi.bowong.cn'
|
||||
export const OWNER_ID = 'x3xbTCWf7dbtWu4gGU2TeI054L77xtkt'
|
||||
export const APP_VERSION = 'dev202512311544'
|
||||
export const APP_VERSION = 'dev202601071100'
|
||||
export const ALIPAY_SCHEMA = 'alipay2021006119657394'
|
||||
export const ALIPAY_SCHEMA_SANDBOX = '9021000158673972'
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import * as FileSystem from 'expo-file-system/legacy'
|
|||
import { Image } from 'expo-image'
|
||||
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'
|
||||
|
|
@ -14,9 +15,12 @@ import { APP_VERSION } from '@/app.constants'
|
|||
import { BLE_UUIDS, PROTOCOL_VERSION, useBleExplorer } from '@/ble'
|
||||
import { ThemedText } from '@/components/themed-text'
|
||||
import { ThemedView } from '@/components/themed-view'
|
||||
import { userStore } from '@/stores'
|
||||
import { uploadFile } from '@/utils'
|
||||
import { buildCdnUrl } from '@/utils/getCDNKey'
|
||||
|
||||
export default function TabTwoScreen() {
|
||||
export default observer(function TabTwoScreen() {
|
||||
const { user } = userStore
|
||||
const {
|
||||
isScanning,
|
||||
isConnected,
|
||||
|
|
@ -45,7 +49,7 @@ export default function TabTwoScreen() {
|
|||
const [imageUri, setImageUri] = useState(
|
||||
'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 () => {
|
||||
|
|
@ -137,7 +141,11 @@ export default function TabTwoScreen() {
|
|||
try {
|
||||
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
|
||||
if (asset?.uri) {
|
||||
|
|
@ -147,7 +155,7 @@ export default function TabTwoScreen() {
|
|||
const url = await uploadFile({
|
||||
uri: asset.uri,
|
||||
mimeType: asset.mimeType,
|
||||
fileName: asset.fileName,
|
||||
fileName: asset.fileName ?? undefined,
|
||||
})
|
||||
|
||||
transferMediaSingle(url)
|
||||
|
|
@ -155,15 +163,18 @@ export default function TabTwoScreen() {
|
|||
Toast?.show({ title: '文件传输完成' })
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error === 'no_space') {
|
||||
const { status } = error || {}
|
||||
if (!status) {
|
||||
Toast?.show({ title: `文件传输失败` })
|
||||
return
|
||||
}
|
||||
|
||||
if (status === 'no_space') {
|
||||
Toast?.show({ title: '文件传输失败, 设备存储空间不足' })
|
||||
return
|
||||
}
|
||||
if (error === 'duplicated') {
|
||||
if (status === 'duplicated') {
|
||||
Toast?.show({ title: '文件传输失败, 设备已存在相同文件' })
|
||||
return
|
||||
}
|
||||
Toast?.show({ title: `文件传输失败,${error}` })
|
||||
})
|
||||
.finally(() => {
|
||||
Toast.hideLoading()
|
||||
|
|
@ -370,14 +381,15 @@ export default function TabTwoScreen() {
|
|||
// .fill('https://cdn.roasmax.cn/material/8836b879f2d44af48eef8da82d13a755.mp4')
|
||||
contents.map((item, index) => {
|
||||
console.log('item-----------', item)
|
||||
|
||||
const url = buildCdnUrl(item)
|
||||
if (!url) return null
|
||||
const Width = 100
|
||||
return (
|
||||
<Block key={`${item}-${index}`} className="gap-2">
|
||||
<ThemedText style={styles.contentText} numberOfLines={1}>
|
||||
{item}
|
||||
</ThemedText>
|
||||
<VideoBox style={{ width: Width, height: Width }} url={item} />
|
||||
<VideoBox style={{ width: Width, height: Width }} url={url} />
|
||||
<Button
|
||||
title="删除"
|
||||
color="red"
|
||||
|
|
@ -410,7 +422,7 @@ export default function TabTwoScreen() {
|
|||
</ThemedView>
|
||||
</KeyboardAwareScrollView>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerImage: {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Alert, Linking, PermissionsAndroid, Platform } from 'react-native'
|
|||
import { ScanMode } from 'react-native-ble-plx'
|
||||
|
||||
import { aniStorage } from '@/utils/aniStorage'
|
||||
import { extractCdnKey } from '@/utils/getCDNKey'
|
||||
|
||||
import { DeviceInfoService } from '../services/DeviceInfoService'
|
||||
import { FileTransferService } from '../services/FileTransferService'
|
||||
|
|
@ -52,6 +53,16 @@ export const useBleExplorer = () => {
|
|||
// 用于暂存扫描到的设备,避免频繁更新状态
|
||||
const pendingDevicesRef = useRef<BleDevice[]>([])
|
||||
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>({
|
||||
isScanning: false,
|
||||
|
|
@ -312,7 +323,17 @@ export const useBleExplorer = () => {
|
|||
|
||||
const onPrepareTransfer = (response: PrepareTransferResponse) => {
|
||||
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') {
|
||||
console.log('Device is ready to receive file:', response.key)
|
||||
} else if (response.status === 'no_space') {
|
||||
|
|
@ -581,29 +602,19 @@ export const useBleExplorer = () => {
|
|||
}
|
||||
|
||||
try {
|
||||
// 创建一个 Promise 来等待响应
|
||||
// 创建 Promise 并存储到 Map
|
||||
const responsePromise = new Promise<PrepareTransferResponse>((resolve, reject) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>
|
||||
|
||||
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)
|
||||
const timeoutId = setTimeout(() => {
|
||||
pendingPrepareTransfersRef.current.delete(key)
|
||||
reject(new Error('Prepare transfer timeout'))
|
||||
}, 10000) // 10秒超时
|
||||
|
||||
// 先注册 Promise,再发送请求
|
||||
pendingPrepareTransfersRef.current.set(key, { resolve, reject, timeoutId })
|
||||
})
|
||||
|
||||
// 发送预先发送请求
|
||||
// ✅ 关键:先注册 Promise,再发送请求
|
||||
// 这样即使设备立即返回,Promise 也已经准备好了
|
||||
await deviceInfoService.prepareTransfer(state.connectedDevice.id, key, size)
|
||||
|
||||
// 等待响应
|
||||
|
|
@ -754,7 +765,7 @@ export const useBleExplorer = () => {
|
|||
let imageUri = media.uri
|
||||
|
||||
// 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.
|
||||
// Here we check if it is NOT jpeg/jpg
|
||||
const isJpeg =
|
||||
|
|
@ -927,11 +938,16 @@ export const useBleExplorer = () => {
|
|||
|
||||
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') {
|
||||
setState((prev) => ({ ...prev, loading: { ...prev.loading, converting: false } }))
|
||||
console.log('prepareResp not ready-----', prepareResp)
|
||||
return Promise.reject(`${prepareResp.status}`)
|
||||
return Promise.reject({ status: `${prepareResp.status}` })
|
||||
}
|
||||
console.log('prepareResp-----', prepareResp)
|
||||
|
||||
|
|
@ -955,7 +971,7 @@ export const useBleExplorer = () => {
|
|||
return Promise.reject(error.message)
|
||||
}
|
||||
},
|
||||
[state.connectedDevice, fileTransferService, convertToANIAsBuffer, setError],
|
||||
[state.connectedDevice, fileTransferService, convertImgToANIAsBuffer, setError],
|
||||
)
|
||||
|
||||
const clearLogs = useCallback(() => setState((prev) => ({ ...prev, logs: [] })), [])
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface UseUpdateCheckerOptions {
|
|||
}
|
||||
|
||||
export const useUpdateChecker = ({
|
||||
interval = 1 * 30 * 1000, // 30秒
|
||||
interval = 5 * 60 * 1000, // 5分钟
|
||||
enablePeriodicCheck = true,
|
||||
}: UseUpdateCheckerOptions = {}) => {
|
||||
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