feat: Better progress bar

This commit is contained in:
2026-06-09 04:01:44 +07:00
parent c368fc63e1
commit ed487b25df
5 changed files with 90 additions and 81 deletions
+8 -4
View File
@@ -44,7 +44,8 @@ const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({
const { const {
status, status,
message, message,
progress, sendProgress,
confirmProgress,
currentFileIndex, currentFileIndex,
totalFiles, totalFiles,
uploadSteps, uploadSteps,
@@ -96,7 +97,8 @@ const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({
<UploadProgress <UploadProgress
status={status} status={status}
message={message} message={message}
progress={progress} sendProgress={sendProgress}
confirmProgress={confirmProgress}
/> />
<ConnectionStatus steps={uploadSteps} /> <ConnectionStatus steps={uploadSteps} />
</Box> </Box>
@@ -107,7 +109,8 @@ const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({
<UploadProgress <UploadProgress
status={status} status={status}
message={message} message={message}
progress={progress} sendProgress={sendProgress}
confirmProgress={confirmProgress}
/> />
<Box marginTop={1}> <Box marginTop={1}>
<Text color="green">Device is ready to use!</Text> <Text color="green">Device is ready to use!</Text>
@@ -120,7 +123,8 @@ const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({
<UploadProgress <UploadProgress
status={status} status={status}
message={message} message={message}
progress={progress} sendProgress={sendProgress}
confirmProgress={confirmProgress}
/> />
<Box marginTop={1}> <Box marginTop={1}>
<Text color="red">Please check your device and try again.</Text> <Text color="red">Please check your device and try again.</Text>
+50 -29
View File
@@ -5,28 +5,45 @@ import Spinner from "ink-spinner";
interface UploadProgressProps { interface UploadProgressProps {
status: "connecting" | "uploading" | "success" | "error"; status: "connecting" | "uploading" | "success" | "error";
message?: string; message?: string;
progress?: number; sendProgress?: number;
confirmProgress?: number;
} }
const ProgressBar: React.FC<{ progress: number }> = ({ progress }) => { const BAR_LENGTH = 30;
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 getBarColor = () => { const ShadowProgressBar: React.FC<{
if (percentage < 100) return "cyan"; sendProgress: number;
return "green"; 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 (
<Text key={i} color="green">
</Text>
);
if (i < sentFilled)
return (
<Text key={i} color="cyan">
</Text>
);
return (
<Text key={i} color="dim">
</Text>
);
});
return ( return (
<Box marginTop={1}> <Box marginTop={1}>
<Text color="dim">[</Text> <Text color="dim">[</Text>
<Text color={getBarColor()}>{barContent}</Text> {bar}
<Text color="dim">] {percentage}%</Text> <Text color="dim">] {pct}%</Text>
</Box> </Box>
); );
}; };
@@ -34,7 +51,8 @@ const ProgressBar: React.FC<{ progress: number }> = ({ progress }) => {
export const UploadProgress: React.FC<UploadProgressProps> = ({ export const UploadProgress: React.FC<UploadProgressProps> = ({
status, status,
message, message,
progress, sendProgress = 0,
confirmProgress = 0,
}) => { }) => {
const getStatusColor = () => { const getStatusColor = () => {
switch (status) { switch (status) {
@@ -49,18 +67,18 @@ export const UploadProgress: React.FC<UploadProgressProps> = ({
} }
}; };
const getStatusIcon = () => { const statusIcon =
switch (status) { status === "success" ? "✓" : status === "error" ? "✗" : null;
case "success":
return "✓";
case "error":
return "✗";
default:
return 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 ( return (
<Box flexDirection="column" paddingY={1}> <Box flexDirection="column" paddingY={1}>
@@ -73,12 +91,15 @@ export const UploadProgress: React.FC<UploadProgressProps> = ({
{statusIcon && <Text color={getStatusColor()}>{statusIcon}</Text>} {statusIcon && <Text color={getStatusColor()}>{statusIcon}</Text>}
<Box marginLeft={1}> <Box marginLeft={1}>
<Text color={getStatusColor()} bold> <Text color={getStatusColor()} bold>
{message || status.toUpperCase()} {displayMessage}
</Text> </Text>
</Box> </Box>
</Box> </Box>
{progress !== undefined && status === "uploading" && ( {showBar && (
<ProgressBar progress={progress} /> <ShadowProgressBar
sendProgress={effectiveSend}
confirmProgress={effectiveConfirm}
/>
)} )}
</Box> </Box>
); );
+15 -9
View File
@@ -12,7 +12,8 @@ export function useUpload(options: UploadOptions, verbose: boolean) {
"connecting" | "uploading" | "success" | "error" "connecting" | "uploading" | "success" | "error"
>("connecting"); >("connecting");
const [message, setMessage] = useState<string>("Initializing..."); const [message, setMessage] = useState<string>("Initializing...");
const [progress, setProgress] = useState<number>(0); const [sendProgress, setSendProgress] = useState<number>(0);
const [confirmProgress, setConfirmProgress] = useState<number>(0);
const [currentFileIndex, setCurrentFileIndex] = useState<number>(0); const [currentFileIndex, setCurrentFileIndex] = useState<number>(0);
const [totalFiles, setTotalFiles] = useState<number>(1); const [totalFiles, setTotalFiles] = useState<number>(1);
@@ -174,14 +175,16 @@ export function useUpload(options: UploadOptions, verbose: boolean) {
}, },
{ id: "complete", label: "Finalizing", status: "pending" }, { id: "complete", label: "Finalizing", status: "pending" },
]); ]);
setProgress(0); setSendProgress(0);
setConfirmProgress(0);
const success = await uploader.uploadImageFromFile( const success = await uploader.uploadImageFromFile(
imagePath, imagePath,
targetSize, targetSize,
animationSize, animationSize,
(prog: number, status?: string) => { (send: number, confirm: number, status?: string) => {
setProgress(prog); setSendProgress(send);
setConfirmProgress(confirm);
if (status) { if (status) {
setMessage(`${i + 1}/${imagesToUpload.length}: ${status}`); setMessage(`${i + 1}/${imagesToUpload.length}: ${status}`);
} }
@@ -215,8 +218,9 @@ export function useUpload(options: UploadOptions, verbose: boolean) {
success = await uploader.uploadCheckerboard( success = await uploader.uploadCheckerboard(
targetSize, targetSize,
8, 8,
(prog: number, status?: string) => { (send: number, confirm: number, status?: string) => {
setProgress(prog); setSendProgress(send);
setConfirmProgress(confirm);
if (status) { if (status) {
setMessage(status); setMessage(status);
} }
@@ -227,8 +231,9 @@ export function useUpload(options: UploadOptions, verbose: boolean) {
options.image, options.image,
targetSize, targetSize,
animationSize, animationSize,
(prog: number, status?: string) => { (send: number, confirm: number, status?: string) => {
setProgress(prog); setSendProgress(send);
setConfirmProgress(confirm);
if (status) { if (status) {
setMessage(status); setMessage(status);
} }
@@ -271,7 +276,8 @@ export function useUpload(options: UploadOptions, verbose: boolean) {
return { return {
status, status,
message, message,
progress, sendProgress,
confirmProgress,
currentFileIndex, currentFileIndex,
totalFiles, totalFiles,
uploadSteps, uploadSteps,
+13 -35
View File
@@ -510,13 +510,13 @@ export class BleUploader {
* Send image data packets to device with batched acknowledgment waiting * Send image data packets to device with batched acknowledgment waiting
* @param fullData Complete image data payload * @param fullData Complete image data payload
* @param packetType Packet type for header * @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 * @returns True if successful
*/ */
public async sendData( public async sendData(
fullData: Buffer, fullData: Buffer,
packetType: PacketType, packetType: PacketType,
onProgress?: (progress: number, status?: string) => void, onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void,
): Promise<boolean> { ): Promise<boolean> {
if (!this.writeCharacteristic) { if (!this.writeCharacteristic) {
throw new ConnectionError("BLE client not connected"); throw new ConnectionError("BLE client not connected");
@@ -589,17 +589,9 @@ export class BleUploader {
// Report progress // Report progress
if (onProgress) { if (onProgress) {
// Sending contributes 50% of this packet's progress const sendProgress = ((i + 1) / totalChunks) * 100;
const sendProgress = ((i + 1) / totalChunks) * 50; const confirmProgress = (this.notificationHandler.packetSuccessCount / totalChunks) * 100;
// Acks contribute the other 50% onProgress(sendProgress, confirmProgress, `Sending: (${i + 1}/${totalChunks} packets)`);
const ackProgress =
(this.notificationHandler.packetSuccessCount / totalChunks) * 50;
const combinedProgress = sendProgress + ackProgress;
onProgress(
combinedProgress,
`Sending: (${i + 1}/${totalChunks} packets)`,
);
} }
await this.sleep(this.chunkDelay * 1000); await this.sleep(this.chunkDelay * 1000);
@@ -623,16 +615,9 @@ export class BleUploader {
} }
if (onProgress) { if (onProgress) {
// Update progress as acks come in const sendProgress = ((i + 1) / totalChunks) * 100;
const sendProgress = ((i + 1) / totalChunks) * 50; const confirmProgress = (this.notificationHandler.packetSuccessCount / totalChunks) * 100;
const ackProgress = onProgress(sendProgress, confirmProgress, `Sending: (${this.notificationHandler.packetSuccessCount}/${totalChunks} packets)`);
(this.notificationHandler.packetSuccessCount / totalChunks) * 50;
const combinedProgress = sendProgress + ackProgress;
onProgress(
combinedProgress,
`Sending: (${this.notificationHandler.packetSuccessCount}/${totalChunks} packets)`,
);
} }
await this.sleep(100); await this.sleep(100);
@@ -651,7 +636,7 @@ export class BleUploader {
* @returns True if all acks received without error * @returns True if all acks received without error
*/ */
public async waitForResponse( public async waitForResponse(
onProgress?: (progress: number, status?: string) => void, onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void,
): Promise<boolean> { ): Promise<boolean> {
this.notificationHandler.waitingForAck = true; this.notificationHandler.waitingForAck = true;
@@ -679,18 +664,11 @@ export class BleUploader {
} }
if (onProgress) { if (onProgress) {
// All packets sent (50%), waiting for remaining acks (0-50%) const confirmProgress =
const sendProgress = 50; // All packets already sent
const ackProgress =
(this.notificationHandler.packetSuccessCount / (this.notificationHandler.packetSuccessCount /
this.notificationHandler.expectedAckCount) * this.notificationHandler.expectedAckCount) *
50; 100;
const combinedProgress = sendProgress + ackProgress; onProgress(100, confirmProgress, `Confirming: (${this.notificationHandler.packetSuccessCount}/${this.notificationHandler.expectedAckCount} packets)`);
onProgress(
combinedProgress,
`Confirming: (${this.notificationHandler.packetSuccessCount}/${this.notificationHandler.expectedAckCount} packets)`,
);
} }
await this.sleep(100); // Check every 100ms await this.sleep(100); // Check every 100ms
@@ -704,7 +682,7 @@ export class BleUploader {
if (success) { if (success) {
logger.info("All acknowledgments received"); logger.info("All acknowledgments received");
if (onProgress) { if (onProgress) {
onProgress(100, "Upload complete"); onProgress(100, 100, "Upload complete");
} }
} else if ( } else if (
this.notificationHandler.packetSuccessCount < this.notificationHandler.packetSuccessCount <
+4 -4
View File
@@ -24,7 +24,7 @@ export interface UploadOptions {
imageData?: Buffer; imageData?: Buffer;
targetSize?: [number, number]; targetSize?: [number, number];
animationSize?: [number, number]; animationSize?: [number, number];
onProgress?: (progress: number, status?: string) => void; onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void;
dumpDir?: string; dumpDir?: string;
} }
@@ -358,7 +358,7 @@ export class BeamBoxUploader {
private async uploadAnimation( private async uploadAnimation(
filePath: string, filePath: string,
targetSize: [number, number], targetSize: [number, number],
onProgress?: (progress: number) => void, onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void,
dumpDir?: string, dumpDir?: string,
): Promise<boolean> { ): Promise<boolean> {
const animationSize: [number, number] = targetSize; const animationSize: [number, number] = targetSize;
@@ -476,7 +476,7 @@ export class BeamBoxUploader {
imagePath: string, imagePath: string,
targetSize?: [number, number], targetSize?: [number, number],
animationSize?: [number, number], animationSize?: [number, number],
onProgress?: (progress: number, status?: string) => void, onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void,
dumpDir?: string, dumpDir?: string,
): Promise<boolean> { ): Promise<boolean> {
return await this.upload({ return await this.upload({
@@ -498,7 +498,7 @@ export class BeamBoxUploader {
public async uploadCheckerboard( public async uploadCheckerboard(
targetSize?: [number, number], targetSize?: [number, number],
squares?: number, squares?: number,
onProgress?: (progress: number, status?: string) => void, onProgress?: (sendProgress: number, confirmProgress: number, status?: string) => void,
dumpDir?: string, dumpDir?: string,
): Promise<boolean> { ): Promise<boolean> {
const effectiveSize = targetSize ?? this.imageConfig.defaultSize; const effectiveSize = targetSize ?? this.imageConfig.defaultSize;