From ed487b25df1ec455c1c3348bb65012695b6d6a57 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Tue, 9 Jun 2026 03:58:56 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Better=20progress=20bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/App.tsx | 12 +++-- src/components/UploadProgress.tsx | 79 +++++++++++++++++++------------ src/hooks/useUpload.ts | 24 ++++++---- src/lib/ble/ble-client.ts | 48 +++++-------------- src/lib/core/beambox-uploader.ts | 8 ++-- 5 files changed, 90 insertions(+), 81 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 0e7e660..1ebed77 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -44,7 +44,8 @@ const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({ const { status, message, - progress, + sendProgress, + confirmProgress, currentFileIndex, totalFiles, uploadSteps, @@ -96,7 +97,8 @@ const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({ @@ -107,7 +109,8 @@ const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({ Device is ready to use! @@ -120,7 +123,8 @@ const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({ Please check your device and try again. diff --git a/src/components/UploadProgress.tsx b/src/components/UploadProgress.tsx index af6328f..1cd7662 100644 --- a/src/components/UploadProgress.tsx +++ b/src/components/UploadProgress.tsx @@ -5,28 +5,45 @@ import Spinner from "ink-spinner"; interface UploadProgressProps { status: "connecting" | "uploading" | "success" | "error"; message?: string; - progress?: number; + sendProgress?: number; + confirmProgress?: number; } -const ProgressBar: React.FC<{ progress: number }> = ({ progress }) => { - const percentage = Math.round(progress); - const barLength = 30; - const filledLength = Math.round((progress / 100) * barLength); - const filled = "█".repeat(filledLength); - const empty = "░".repeat(barLength - filledLength); +const BAR_LENGTH = 30; - const getBarColor = () => { - if (percentage < 100) return "cyan"; - return "green"; - }; +const ShadowProgressBar: React.FC<{ + sendProgress: number; + confirmProgress: number; +}> = ({ sendProgress, confirmProgress }) => { + const sentFilled = Math.round((sendProgress / 100) * BAR_LENGTH); + const confirmFilled = Math.round((confirmProgress / 100) * BAR_LENGTH); + const pct = Math.round(sendProgress); - const barContent = filled + empty; + const bar = Array.from({ length: BAR_LENGTH }, (_, i) => { + if (i < confirmFilled) + return ( + + █ + + ); + if (i < sentFilled) + return ( + + █ + + ); + return ( + + ░ + + ); + }); return ( [ - {barContent} - ] {percentage}% + {bar} + ] {pct}% ); }; @@ -34,7 +51,8 @@ const ProgressBar: React.FC<{ progress: number }> = ({ progress }) => { export const UploadProgress: React.FC = ({ status, message, - progress, + sendProgress = 0, + confirmProgress = 0, }) => { const getStatusColor = () => { switch (status) { @@ -49,18 +67,18 @@ export const UploadProgress: React.FC = ({ } }; - const getStatusIcon = () => { - switch (status) { - case "success": - return "✓"; - case "error": - return "✗"; - default: - return null; - } - }; + const statusIcon = + status === "success" ? "✓" : status === "error" ? "✗" : null; - const statusIcon = getStatusIcon(); + const showBar = status === "uploading" || status === "success"; + const effectiveSend = status === "success" ? 100 : sendProgress; + const effectiveConfirm = status === "success" ? 100 : confirmProgress; + + const isConfirming = + status === "uploading" && sendProgress >= 100 && confirmProgress < 100; + const displayMessage = isConfirming + ? (message?.replace(/^Sending:/, "Confirming:") ?? "Confirming...") + : message || status.toUpperCase(); return ( @@ -73,12 +91,15 @@ export const UploadProgress: React.FC = ({ {statusIcon && {statusIcon}} - {message || status.toUpperCase()} + {displayMessage} - {progress !== undefined && status === "uploading" && ( - + {showBar && ( + )} ); diff --git a/src/hooks/useUpload.ts b/src/hooks/useUpload.ts index 979dfb5..fb97d30 100644 --- a/src/hooks/useUpload.ts +++ b/src/hooks/useUpload.ts @@ -12,7 +12,8 @@ export function useUpload(options: UploadOptions, verbose: boolean) { "connecting" | "uploading" | "success" | "error" >("connecting"); const [message, setMessage] = useState("Initializing..."); - const [progress, setProgress] = useState(0); + const [sendProgress, setSendProgress] = useState(0); + const [confirmProgress, setConfirmProgress] = useState(0); const [currentFileIndex, setCurrentFileIndex] = useState(0); const [totalFiles, setTotalFiles] = useState(1); @@ -174,14 +175,16 @@ export function useUpload(options: UploadOptions, verbose: boolean) { }, { id: "complete", label: "Finalizing", status: "pending" }, ]); - setProgress(0); + setSendProgress(0); + setConfirmProgress(0); const success = await uploader.uploadImageFromFile( imagePath, targetSize, animationSize, - (prog: number, status?: string) => { - setProgress(prog); + (send: number, confirm: number, status?: string) => { + setSendProgress(send); + setConfirmProgress(confirm); if (status) { setMessage(`${i + 1}/${imagesToUpload.length}: ${status}`); } @@ -215,8 +218,9 @@ export function useUpload(options: UploadOptions, verbose: boolean) { success = await uploader.uploadCheckerboard( targetSize, 8, - (prog: number, status?: string) => { - setProgress(prog); + (send: number, confirm: number, status?: string) => { + setSendProgress(send); + setConfirmProgress(confirm); if (status) { setMessage(status); } @@ -227,8 +231,9 @@ export function useUpload(options: UploadOptions, verbose: boolean) { options.image, targetSize, animationSize, - (prog: number, status?: string) => { - setProgress(prog); + (send: number, confirm: number, status?: string) => { + setSendProgress(send); + setConfirmProgress(confirm); if (status) { setMessage(status); } @@ -271,7 +276,8 @@ export function useUpload(options: UploadOptions, verbose: boolean) { return { status, message, - progress, + sendProgress, + confirmProgress, currentFileIndex, totalFiles, uploadSteps, diff --git a/src/lib/ble/ble-client.ts b/src/lib/ble/ble-client.ts index f37b3e3..79021c1 100644 --- a/src/lib/ble/ble-client.ts +++ b/src/lib/ble/ble-client.ts @@ -510,13 +510,13 @@ export class BleUploader { * Send image data packets to device with batched acknowledgment waiting * @param fullData Complete image data payload * @param packetType Packet type for header - * @param onProgress Progress callback with (progress, status) + * @param onProgress Progress callback with (sendProgress 0-100, confirmProgress 0-100, status) * @returns True if successful */ public async sendData( fullData: Buffer, packetType: PacketType, - onProgress?: (progress: number, status?: string) => void, + onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void, ): Promise { if (!this.writeCharacteristic) { throw new ConnectionError("BLE client not connected"); @@ -589,17 +589,9 @@ export class BleUploader { // Report progress if (onProgress) { - // Sending contributes 50% of this packet's progress - const sendProgress = ((i + 1) / totalChunks) * 50; - // Acks contribute the other 50% - const ackProgress = - (this.notificationHandler.packetSuccessCount / totalChunks) * 50; - const combinedProgress = sendProgress + ackProgress; - - onProgress( - combinedProgress, - `Sending: (${i + 1}/${totalChunks} packets)`, - ); + const sendProgress = ((i + 1) / totalChunks) * 100; + const confirmProgress = (this.notificationHandler.packetSuccessCount / totalChunks) * 100; + onProgress(sendProgress, confirmProgress, `Sending: (${i + 1}/${totalChunks} packets)`); } await this.sleep(this.chunkDelay * 1000); @@ -623,16 +615,9 @@ export class BleUploader { } if (onProgress) { - // Update progress as acks come in - const sendProgress = ((i + 1) / totalChunks) * 50; - const ackProgress = - (this.notificationHandler.packetSuccessCount / totalChunks) * 50; - const combinedProgress = sendProgress + ackProgress; - - onProgress( - combinedProgress, - `Sending: (${this.notificationHandler.packetSuccessCount}/${totalChunks} packets)`, - ); + const sendProgress = ((i + 1) / totalChunks) * 100; + const confirmProgress = (this.notificationHandler.packetSuccessCount / totalChunks) * 100; + onProgress(sendProgress, confirmProgress, `Sending: (${this.notificationHandler.packetSuccessCount}/${totalChunks} packets)`); } await this.sleep(100); @@ -651,7 +636,7 @@ export class BleUploader { * @returns True if all acks received without error */ public async waitForResponse( - onProgress?: (progress: number, status?: string) => void, + onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void, ): Promise { this.notificationHandler.waitingForAck = true; @@ -679,18 +664,11 @@ export class BleUploader { } if (onProgress) { - // All packets sent (50%), waiting for remaining acks (0-50%) - const sendProgress = 50; // All packets already sent - const ackProgress = + const confirmProgress = (this.notificationHandler.packetSuccessCount / this.notificationHandler.expectedAckCount) * - 50; - const combinedProgress = sendProgress + ackProgress; - - onProgress( - combinedProgress, - `Confirming: (${this.notificationHandler.packetSuccessCount}/${this.notificationHandler.expectedAckCount} packets)`, - ); + 100; + onProgress(100, confirmProgress, `Confirming: (${this.notificationHandler.packetSuccessCount}/${this.notificationHandler.expectedAckCount} packets)`); } await this.sleep(100); // Check every 100ms @@ -704,7 +682,7 @@ export class BleUploader { if (success) { logger.info("All acknowledgments received"); if (onProgress) { - onProgress(100, "Upload complete"); + onProgress(100, 100, "Upload complete"); } } else if ( this.notificationHandler.packetSuccessCount < diff --git a/src/lib/core/beambox-uploader.ts b/src/lib/core/beambox-uploader.ts index 75c7460..a94259d 100644 --- a/src/lib/core/beambox-uploader.ts +++ b/src/lib/core/beambox-uploader.ts @@ -24,7 +24,7 @@ export interface UploadOptions { imageData?: Buffer; targetSize?: [number, number]; animationSize?: [number, number]; - onProgress?: (progress: number, status?: string) => void; + onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void; dumpDir?: string; } @@ -358,7 +358,7 @@ export class BeamBoxUploader { private async uploadAnimation( filePath: string, targetSize: [number, number], - onProgress?: (progress: number) => void, + onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void, dumpDir?: string, ): Promise { const animationSize: [number, number] = targetSize; @@ -476,7 +476,7 @@ export class BeamBoxUploader { imagePath: string, targetSize?: [number, number], animationSize?: [number, number], - onProgress?: (progress: number, status?: string) => void, + onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void, dumpDir?: string, ): Promise { return await this.upload({ @@ -498,7 +498,7 @@ export class BeamBoxUploader { public async uploadCheckerboard( targetSize?: [number, number], squares?: number, - onProgress?: (progress: number, status?: string) => void, + onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void, dumpDir?: string, ): Promise { const effectiveSize = targetSize ?? this.imageConfig.defaultSize;