diff --git a/app.config.js b/app.config.js index c2f643b..e510ab3 100644 --- a/app.config.js +++ b/app.config.js @@ -36,6 +36,7 @@ export default ({ config }) => { ios: { supportsTablet: true, infoPlist: { + UIViewControllerBasedStatusBarAppearance: false, NSBluetoothPeripheralUsageDescription: 'This app uses Bluetooth to act as a peripheral device for testing and development purposes.', CFBundleURLTypes: [ diff --git a/app/(tabs)/sync.tsx b/app/(tabs)/sync.tsx index ea914a6..6b17781 100644 --- a/app/(tabs)/sync.tsx +++ b/app/(tabs)/sync.tsx @@ -31,12 +31,18 @@ import { screenWidth, uploadFile } from '@/utils' import { cn } from '@/utils/cn' import { extractCdnKey } from '@/utils/getCDNKey' -const ITEM_WIDTH = Math.floor((screenWidth - 12 * 2 - 12 * 2) / 3) +const ITEM_GAP = 6 +// FlashList 会将可用宽度(screenWidth - padding*2)平均分配给每列 +// 所以每个item容器的宽度是 (screenWidth - 24) / 3 +// item实际显示宽度需要减去间距 +const ITEM_CONTAINER_WIDTH = Math.floor((screenWidth - 12 * 2) / 3) +const ITEM_WIDTH = ITEM_CONTAINER_WIDTH - ITEM_GAP // ============ 主组件 ============ const Sync = observer(() => { // 从MobX Store获取用户信息 const { user, isLogin } = userStore + const insets = useSafeAreaInsets() const { data: generationsData, loading: generationsLoading, @@ -63,17 +69,6 @@ const Sync = observer(() => { }) const [isSelectionMode, setIsSelectionMode] = useState(false) const [selectedIds, setSelectedIds] = useState>(new Set()) - const [visibleIds, setVisibleIds] = useState>(new Set()) - - const onViewableItemsChanged = useRef(({ viewableItems }: any) => { - const ids = new Set(viewableItems.map((item: any) => item.item?.id).filter(Boolean) as string[]) - setVisibleIds(ids) - }).current - - const viewabilityConfig = useRef({ - itemVisiblePercentThreshold: 10, - minimumViewTime: 100, - }).current // 直接使用 useIsFocused hook,页面失焦时不渲染列表项以减少内存占用 const isFocused = useIsFocused() @@ -416,9 +411,7 @@ const Sync = observer(() => { isSelectionMode={isSelectionMode} itemWidth={ITEM_WIDTH} post={post} - // 页面失焦时不渲染,减少内存占用 onSelect={handleItemSelect} - isVisible={visibleIds.has(post?.id)} /> ) } @@ -427,18 +420,17 @@ const Sync = observer(() => { return null } + // 计算底部内边距:FAB按钮高度(56) + 按钮底部距离(96) + 安全区域 + 额外间距 + const listPaddingBottom = 56 + 96 + insets.bottom + 20 + return ( } keyExtractor={(item: any) => item?.id} ListFooterComponent={ListFooter} ListHeaderComponent={renderHeader} @@ -450,8 +442,6 @@ const Sync = observer(() => { } onEndReached={onLoadMore} onEndReachedThreshold={0.3} - onViewableItemsChanged={onViewableItemsChanged} - viewabilityConfig={viewabilityConfig} /> @@ -681,14 +671,12 @@ const GridItem = memo( isSelected, isSelectionMode, itemWidth, - isVisible = true, onSelect, }: { post: any isSelected: boolean isSelectionMode: boolean itemWidth: number - isVisible?: boolean onSelect: (post: any) => void }) => { // 渲染状态标记 @@ -733,9 +721,14 @@ const GridItem = memo( const placeholderSrc = null // 新数据使用webp 旧数据使用mp4 - const canShow = (post.status === 'completed' || post.status === 'success') && isVisible + const canShow = post.status === 'completed' || post.status === 'success' return ( - onSelect(post)}> + onSelect(post)} + > + 已选: {selectedCount} @@ -852,7 +845,7 @@ const SelectionBar = memo( const FABButtons = memo(({ onGenAgain, handleSync }: { onGenAgain: () => void; handleSync: () => void }) => { const insets = useSafeAreaInsets() return ( - + void, + ): Promise { + const total = frames.length + const maxRetries = FRAME_CONSTANTS.FLOW_CONTROL.MAX_RETRIES + const retryDelay = FRAME_CONSTANTS.FLOW_CONTROL.RETRY_DELAY + + // 性能监控指标 + const startTime = Date.now() + let failedCount = 0 + let retriedCount = 0 + let totalRetries = 0 + + // 自适应间隔参数 + const MIN_INTERVAL = 40 // 最小间隔 40ms + const MAX_INTERVAL = 150 // 最大间隔 150ms + const INITIAL_INTERVAL = 50 // 初始间隔 50ms(比原来的35ms保守) + let currentInterval = INITIAL_INTERVAL + let consecutiveSuccesses = 0 + let consecutiveFailures = 0 + + console.debug(`[FlowControl] Starting serial transmission: ${total} frames, initial interval: ${currentInterval}ms`) + for (let i = 0; i < total; i++) { const frame = frames[i] + + // 打印前几帧的原始数据用于调试 if (i < 3) { const rawFrame = Array.from(frame) .map((b) => b.toString(16).padStart(2, '0')) .join(' ') console.debug(`raw ${i + 1} frame \n ${rawFrame}`) } - // console.debug(`Writing frame ${i + 1}/${total}, length = ${frame.length}`) + const base64 = Buffer.from(frame).toString('base64') - const result = await this.client.write(deviceId, BLE_UUIDS.SERVICE, BLE_UUIDS.WRITE_CHARACTERISTIC, base64, false) - await new Promise((resolve) => setTimeout(resolve, FRAME_CONSTANTS.FRAME_INTERVAL)) + let frameSucceeded = false + + // 重试逻辑(第一帧失败直接报错,不重试) + const frameMaxRetries = i === 0 ? 0 : maxRetries + + for (let attempt = 0; attempt <= frameMaxRetries; attempt++) { + try { + // 串行等待每次写入完成 + await this.client.write(deviceId, BLE_UUIDS.SERVICE, BLE_UUIDS.WRITE_CHARACTERISTIC, base64, false) + + // 写入成功 + frameSucceeded = true + consecutiveSuccesses++ + consecutiveFailures = 0 + + if (attempt > 0) { + retriedCount++ + totalRetries += attempt + console.debug(`[FlowControl] Frame ${i} succeeded after ${attempt} retries`) + } + + // 自适应调整:连续成功 5 次,逐渐减小间隔(加速) + if (consecutiveSuccesses >= 5 && currentInterval > MIN_INTERVAL) { + currentInterval = Math.max(MIN_INTERVAL, currentInterval - 5) + console.debug(`[FlowControl] Speed up: interval reduced to ${currentInterval}ms`) + consecutiveSuccesses = 0 + } + + break + } catch (error) { + // 第一帧失败直接报错 + if (i === 0) { + console.error(`[FlowControl] First frame write failed, aborting:`, error) + throw new Error(`First frame write failed: ${(error as Error)?.message || error}`) + } + + consecutiveFailures++ + consecutiveSuccesses = 0 + + console.warn(`[FlowControl] Frame ${i} write failed (attempt ${attempt + 1}/${frameMaxRetries + 1}):`, error) + + // 如果还有重试机会,等待后重试 + if (attempt < frameMaxRetries) { + const backoffDelay = retryDelay * (attempt + 1) + await new Promise((resolve) => setTimeout(resolve, backoffDelay)) + } else { + // 所有重试都失败 + failedCount++ + console.error(`[FlowControl] Frame ${i} failed after ${frameMaxRetries} retries`) + + // 严重失败:大幅增加间隔 + currentInterval = Math.min(MAX_INTERVAL, currentInterval * 2) + console.warn(`[FlowControl] Severe failure: interval increased to ${currentInterval}ms`) + } + } + } + + // 自适应调整:连续失败,增加间隔(退避) + if (consecutiveFailures >= 2 && currentInterval < MAX_INTERVAL) { + currentInterval = Math.min(MAX_INTERVAL, currentInterval * 1.5) + console.debug(`[FlowControl] Slow down: interval increased to ${currentInterval}ms`) + } + + // 间隔等待(只有成功才继续,失败会在重试中已经等待过) + if (frameSucceeded) { + await new Promise((resolve) => setTimeout(resolve, currentInterval)) + } + + // 更新进度 if (onProgress) { onProgress((i + 1) / total) } - // console.debug("Wrote frame", result); + } + + // 性能统计 + const duration = Date.now() - startTime + const throughput = total > 0 ? (total / duration) * 1000 : 0 + const avgRetriesPerFailedFrame = failedCount > 0 ? totalRetries / retriedCount : 0 + + console.log( + `[FlowControl] Transmission completed: + - Total frames: ${total} + - Duration: ${duration}ms + - Throughput: ${throughput.toFixed(2)} frames/sec + - Failed: ${failedCount} + - Frames retried: ${retriedCount} + - Total retry attempts: ${totalRetries} + - Avg retries per failed frame: ${avgRetriesPerFailedFrame.toFixed(2)} + - Final interval: ${currentInterval}ms + - Success rate: ${(((total - failedCount) / total) * 100).toFixed(2)}%`, + ) + + if (failedCount > 0) { + throw new Error(`Transmission incomplete: ${failedCount}/${total} frames failed`) } } }