mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add progress bar to collage processing
Uses XMLHttpRequest upload progress to track image upload (0-80%), then shows "Processing collage..." for the server-side compositing phase (80-100%). Progress bar with percentage shown in the preview area.
This commit is contained in:
@@ -61,12 +61,7 @@ export function CollagePreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (phase === "processing") {
|
if (phase === "processing") {
|
||||||
return (
|
return <ProcessingView />;
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-3">
|
|
||||||
<Loader2 className="h-8 w-8 text-primary animate-spin" />
|
|
||||||
<p className="text-sm text-muted-foreground">Creating your collage...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (phase === "result" && resultUrl) {
|
if (phase === "result" && resultUrl) {
|
||||||
@@ -150,6 +145,29 @@ function UploadArea() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ProcessingView() {
|
||||||
|
const progress = useCollageStore((s) => s.progress);
|
||||||
|
const label = progress < 80 ? "Uploading images..." : "Processing collage...";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full gap-4 px-8">
|
||||||
|
<Loader2 className="h-8 w-8 text-primary animate-spin" />
|
||||||
|
<div className="w-full max-w-xs space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||||
|
<span>{label}</span>
|
||||||
|
<span className="font-mono">{progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 w-full rounded-full bg-muted overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-primary transition-all duration-300 ease-out"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** The live CSS Grid collage canvas. */
|
/** The live CSS Grid collage canvas. */
|
||||||
function CollageCanvas({ template }: { template: CollageTemplate }) {
|
function CollageCanvas({ template }: { template: CollageTemplate }) {
|
||||||
const store = useCollageStore();
|
const store = useCollageStore();
|
||||||
|
|||||||
@@ -63,11 +63,11 @@ export function CollageSettings() {
|
|||||||
if (!hasImages || !template) return;
|
if (!hasImages || !template) return;
|
||||||
|
|
||||||
store.setPhase("processing");
|
store.setPhase("processing");
|
||||||
|
store.setProgress(0);
|
||||||
store.setError(null);
|
store.setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
// Send images in cell-assignment order
|
|
||||||
for (let i = 0; i < template.cells.length; i++) {
|
for (let i = 0; i < template.cells.length; i++) {
|
||||||
const imgIdx = cellAssignments[i] ?? -1;
|
const imgIdx = cellAssignments[i] ?? -1;
|
||||||
if (imgIdx >= 0 && images[imgIdx]) {
|
if (imgIdx >= 0 && images[imgIdx]) {
|
||||||
@@ -94,18 +94,50 @@ export function CollageSettings() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const res = await fetch("/api/v1/tools/collage", {
|
const result = await new Promise<{
|
||||||
method: "POST",
|
downloadUrl: string;
|
||||||
headers: formatHeaders(),
|
processedSize: number;
|
||||||
body: formData,
|
originalSize: number;
|
||||||
|
jobId: string;
|
||||||
|
}>((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", "/api/v1/tools/collage");
|
||||||
|
|
||||||
|
const headers = formatHeaders();
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
xhr.setRequestHeader(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.upload.onprogress = (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
store.setProgress(Math.round((e.loaded / e.total) * 80));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
store.setProgress(100);
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(xhr.responseText));
|
||||||
|
} catch {
|
||||||
|
reject(new Error("Invalid response"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(xhr.responseText);
|
||||||
|
reject(new Error(body.error || `Failed: ${xhr.status}`));
|
||||||
|
} catch {
|
||||||
|
reject(new Error(`Failed: ${xhr.status}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = () => reject(new Error("Network error"));
|
||||||
|
xhr.send(formData);
|
||||||
|
|
||||||
|
store.setProgress(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const body = await res.json().catch(() => ({}));
|
|
||||||
throw new Error(body.error || `Failed: ${res.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await res.json();
|
|
||||||
store.setResult(result.downloadUrl, result.processedSize, result.originalSize, result.jobId);
|
store.setResult(result.downloadUrl, result.processedSize, result.originalSize, result.jobId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
store.setError(err instanceof Error ? err.message : "Collage failed");
|
store.setError(err instanceof Error ? err.message : "Collage failed");
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ interface CollageState {
|
|||||||
// UI state
|
// UI state
|
||||||
selectedCell: number | null;
|
selectedCell: number | null;
|
||||||
phase: Phase;
|
phase: Phase;
|
||||||
|
progress: number;
|
||||||
resultUrl: string | null;
|
resultUrl: string | null;
|
||||||
resultSize: number | null;
|
resultSize: number | null;
|
||||||
originalSize: number | null;
|
originalSize: number | null;
|
||||||
@@ -74,6 +75,7 @@ interface CollageState {
|
|||||||
setQuality: (v: number) => void;
|
setQuality: (v: number) => void;
|
||||||
setSelectedCell: (v: number | null) => void;
|
setSelectedCell: (v: number | null) => void;
|
||||||
setPhase: (v: Phase) => void;
|
setPhase: (v: Phase) => void;
|
||||||
|
setProgress: (v: number) => void;
|
||||||
setResult: (url: string, size: number, originalSize: number, jobId: string) => void;
|
setResult: (url: string, size: number, originalSize: number, jobId: string) => void;
|
||||||
setError: (e: string | null) => void;
|
setError: (e: string | null) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
@@ -105,6 +107,7 @@ export const useCollageStore = create<CollageState>((set, get) => ({
|
|||||||
quality: 90,
|
quality: 90,
|
||||||
selectedCell: null,
|
selectedCell: null,
|
||||||
phase: "upload",
|
phase: "upload",
|
||||||
|
progress: 0,
|
||||||
resultUrl: null,
|
resultUrl: null,
|
||||||
resultSize: null,
|
resultSize: null,
|
||||||
originalSize: null,
|
originalSize: null,
|
||||||
@@ -263,6 +266,7 @@ export const useCollageStore = create<CollageState>((set, get) => ({
|
|||||||
setQuality: (v) => set({ quality: v, resultUrl: null }),
|
setQuality: (v) => set({ quality: v, resultUrl: null }),
|
||||||
setSelectedCell: (v) => set({ selectedCell: v }),
|
setSelectedCell: (v) => set({ selectedCell: v }),
|
||||||
setPhase: (v) => set({ phase: v }),
|
setPhase: (v) => set({ phase: v }),
|
||||||
|
setProgress: (v) => set({ progress: v }),
|
||||||
setResult: (url, size, originalSize, jobId) =>
|
setResult: (url, size, originalSize, jobId) =>
|
||||||
set({ resultUrl: url, resultSize: size, originalSize, jobId, phase: "result", error: null }),
|
set({ resultUrl: url, resultSize: size, originalSize, jobId, phase: "result", error: null }),
|
||||||
setError: (e) => set({ error: e, phase: "editing" }),
|
setError: (e) => set({ error: e, phase: "editing" }),
|
||||||
@@ -287,6 +291,7 @@ export const useCollageStore = create<CollageState>((set, get) => ({
|
|||||||
quality: 90,
|
quality: 90,
|
||||||
selectedCell: null,
|
selectedCell: null,
|
||||||
phase: "upload",
|
phase: "upload",
|
||||||
|
progress: 0,
|
||||||
resultUrl: null,
|
resultUrl: null,
|
||||||
resultSize: null,
|
resultSize: null,
|
||||||
originalSize: null,
|
originalSize: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user