mirror of
https://github.com/YuzuZensai/beamboxctl.git
synced 2026-07-21 20:42:19 +00:00
✨ feat: Better progress bar
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 <
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user