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 {
status,
message,
progress,
sendProgress,
confirmProgress,
currentFileIndex,
totalFiles,
uploadSteps,
@@ -96,7 +97,8 @@ const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({
<UploadProgress
status={status}
message={message}
progress={progress}
sendProgress={sendProgress}
confirmProgress={confirmProgress}
/>
<ConnectionStatus steps={uploadSteps} />
</Box>
@@ -107,7 +109,8 @@ const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({
<UploadProgress
status={status}
message={message}
progress={progress}
sendProgress={sendProgress}
confirmProgress={confirmProgress}
/>
<Box marginTop={1}>
<Text color="green">Device is ready to use!</Text>
@@ -120,7 +123,8 @@ const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({
<UploadProgress
status={status}
message={message}
progress={progress}
sendProgress={sendProgress}
confirmProgress={confirmProgress}
/>
<Box marginTop={1}>
<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 {
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 (
<Text key={i} color="green">
</Text>
);
if (i < sentFilled)
return (
<Text key={i} color="cyan">
</Text>
);
return (
<Text key={i} color="dim">
</Text>
);
});
return (
<Box marginTop={1}>
<Text color="dim">[</Text>
<Text color={getBarColor()}>{barContent}</Text>
<Text color="dim">] {percentage}%</Text>
{bar}
<Text color="dim">] {pct}%</Text>
</Box>
);
};
@@ -34,7 +51,8 @@ const ProgressBar: React.FC<{ progress: number }> = ({ progress }) => {
export const UploadProgress: React.FC<UploadProgressProps> = ({
status,
message,
progress,
sendProgress = 0,
confirmProgress = 0,
}) => {
const getStatusColor = () => {
switch (status) {
@@ -49,18 +67,18 @@ export const UploadProgress: React.FC<UploadProgressProps> = ({
}
};
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 (
<Box flexDirection="column" paddingY={1}>
@@ -73,12 +91,15 @@ export const UploadProgress: React.FC<UploadProgressProps> = ({
{statusIcon && <Text color={getStatusColor()}>{statusIcon}</Text>}
<Box marginLeft={1}>
<Text color={getStatusColor()} bold>
{message || status.toUpperCase()}
{displayMessage}
</Text>
</Box>
</Box>
{progress !== undefined && status === "uploading" && (
<ProgressBar progress={progress} />
{showBar && (
<ShadowProgressBar
sendProgress={effectiveSend}
confirmProgress={effectiveConfirm}
/>
)}
</Box>
);
+15 -9
View File
@@ -12,7 +12,8 @@ export function useUpload(options: UploadOptions, verbose: boolean) {
"connecting" | "uploading" | "success" | "error"
>("connecting");
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 [totalFiles, setTotalFiles] = useState<number>(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,
+13 -35
View File
@@ -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<boolean> {
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<boolean> {
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 <
+4 -4
View File
@@ -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<boolean> {
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<boolean> {
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<boolean> {
const effectiveSize = targetSize ?? this.imageConfig.defaultSize;