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 {
|
||||
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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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 <
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user