Skip to content

Commit 43722df

Browse files
committed
Polish translate screenshots dialog layout and fix translate all scope
- Single horizontal scroll row with auto-scroll to active item - Inline progress bar in footer: Translating (up to 1-2 min per image) [bar] 2/7 - Fix translate all to only include current variant, not all variants - Fixed dialog width at max-w-4xl
1 parent e22cc76 commit 43722df

2 files changed

Lines changed: 98 additions & 112 deletions

File tree

src/app/dashboard/apps/[appId]/screenshots/page.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -668,13 +668,11 @@ function BaseLocaleScreenshots({
668668
}
669669

670670
function handleTranslateAll() {
671-
// Collect all screenshots across all variants
671+
if (!selectedSet) return;
672672
const items: ScreenshotItem[] = [];
673-
for (const set of setsWithScreenshots) {
674-
for (const ss of set.screenshots) {
675-
if (ss.attributes.assetDeliveryState?.state === "COMPLETE" && ss.attributes.assetToken) {
676-
items.push({ screenshot: ss, displayType: set.attributes.screenshotDisplayType });
677-
}
673+
for (const ss of selectedSet.screenshots) {
674+
if (ss.attributes.assetDeliveryState?.state === "COMPLETE" && ss.attributes.assetToken) {
675+
items.push({ screenshot: ss, displayType: selectedType });
678676
}
679677
}
680678
openTranslateModal(items);

src/components/translate-screenshots-modal.tsx

Lines changed: 94 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, useEffect, useRef, useCallback } from "react";
3+
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
44
import {
55
Eye, EyeSlash, Info, CheckCircle, XCircle,
66
ArrowsClockwise,
@@ -16,6 +16,7 @@ import {
1616
DialogHeader,
1717
DialogTitle,
1818
} from "@/components/ui/dialog";
19+
import { cn } from "@/lib/utils";
1920
import { localeName } from "@/lib/asc/locale-names";
2021
import { screenshotImageUrl } from "@/lib/asc/display-types";
2122
import type { AscScreenshot } from "@/lib/asc/display-types";
@@ -70,6 +71,7 @@ export function TranslateScreenshotsModal({
7071
const stopRef = useRef(false);
7172
const abortRef = useRef<AbortController | null>(null);
7273
const setIdCacheRef = useRef(new Map<string, string>());
74+
const scrollRef = useRef<HTMLDivElement>(null);
7375

7476
// Gemini key state
7577
const [geminiKey, setGeminiKey] = useState("");
@@ -300,6 +302,27 @@ export function TranslateScreenshotsModal({
300302
processItems(false, geminiKey.trim());
301303
}
302304

305+
// Auto-scroll to the latest in-progress or completed item
306+
const lastActiveIndex = useMemo(() => {
307+
let idx = -1;
308+
for (let i = 0; i < items.length; i++) {
309+
const s = itemStates.get(items[i].screenshot.id)?.status;
310+
if (s === "translating" || s === "uploading" || s === "done" || s === "failed") {
311+
idx = i;
312+
}
313+
}
314+
return idx;
315+
}, [items, itemStates]);
316+
317+
useEffect(() => {
318+
if (lastActiveIndex < 0 || !scrollRef.current) return;
319+
const container = scrollRef.current;
320+
const child = container.children[lastActiveIndex] as HTMLElement | undefined;
321+
if (child) {
322+
child.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
323+
}
324+
}, [lastActiveIndex]);
325+
303326
// Compute progress stats
304327
const total = items.length;
305328
const doneCount = Array.from(itemStates.values()).filter((s) => s.status === "done").length;
@@ -310,117 +333,75 @@ export function TranslateScreenshotsModal({
310333

311334
return (
312335
<Dialog open={open} onOpenChange={(o) => { if (!o) handleClose(); }}>
313-
<DialogContent className="sm:max-w-4xl max-h-[90vh] flex flex-col">
336+
<DialogContent className="sm:max-w-4xl w-full max-h-[90vh] flex flex-col">
314337
<DialogHeader>
315338
<DialogTitle>
316339
{isSingle ? "Translate screenshot" : `Translate ${total} screenshots`}
317340
{" "}to {localeName(toLocale)}
318341
</DialogTitle>
319342
</DialogHeader>
320343

321-
{/* Progress grid */}
322-
{started && (
323-
<div className="flex-1 overflow-y-auto">
324-
{/* Progress bar */}
325-
<div className="mb-3 space-y-1.5">
326-
<div className="flex items-center justify-between text-xs text-muted-foreground">
327-
<span>
328-
{doneCount} of {total} completed
329-
{failedCount > 0 && ` (${failedCount} failed)`}
330-
</span>
331-
</div>
332-
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
333-
<div
334-
className="h-full rounded-full bg-primary transition-all duration-300"
335-
style={{ width: `${total > 0 ? ((doneCount + failedCount) / total) * 100 : 0}%` }}
336-
/>
337-
</div>
338-
</div>
339-
340-
{/* Thumbnail grid */}
341-
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
342-
{items.map((item) => {
343-
const state = itemStates.get(item.screenshot.id);
344-
const status = state?.status ?? "queued";
345-
const token = item.screenshot.attributes.assetToken;
346-
347-
return (
348-
<div
349-
key={item.screenshot.id}
350-
className="relative flex flex-col items-center rounded-lg border bg-muted/20 p-2"
351-
>
352-
{/* Show translated thumbnail if done, otherwise original */}
353-
{status === "done" && state?.thumbnail ? (
354-
<img
355-
src={`data:${state.thumbnailMimeType};base64,${state.thumbnail}`}
356-
alt="Translated"
357-
className="h-[300px] w-auto rounded object-contain"
358-
/>
359-
) : token ? (
360-
<img
361-
src={screenshotImageUrl(token, 400)}
362-
alt={item.screenshot.attributes.fileName}
363-
className="h-[300px] w-auto rounded object-contain opacity-40"
364-
/>
365-
) : (
366-
<div className="flex h-[300px] w-[168px] items-center justify-center rounded bg-muted">
367-
<Spinner className="size-6 text-muted-foreground/40" />
368-
</div>
344+
{/* Horizontal screenshot row */}
345+
<div ref={scrollRef} className="flex gap-3 overflow-x-auto pb-2">
346+
{items.map((item) => {
347+
const state = itemStates.get(item.screenshot.id);
348+
const status = started ? (state?.status ?? "queued") : "idle";
349+
const token = item.screenshot.attributes.assetToken;
350+
351+
return (
352+
<div
353+
key={item.screenshot.id}
354+
className="relative shrink-0 flex flex-col items-center rounded-lg border bg-muted/20 p-2"
355+
>
356+
{status === "done" && state?.thumbnail ? (
357+
<img
358+
src={`data:${state.thumbnailMimeType};base64,${state.thumbnail}`}
359+
alt="Translated"
360+
className="h-[300px] w-auto rounded object-contain"
361+
/>
362+
) : token ? (
363+
<img
364+
src={screenshotImageUrl(token, 400)}
365+
alt={item.screenshot.attributes.fileName}
366+
className={cn(
367+
"h-[300px] w-auto rounded object-contain",
368+
started && status !== "done" && "opacity-40",
369369
)}
370+
/>
371+
) : (
372+
<div className="flex h-[300px] w-[168px] items-center justify-center rounded bg-muted">
373+
<Spinner className="size-6 text-muted-foreground/40" />
374+
</div>
375+
)}
370376

371-
{/* Status overlay */}
372-
<div className="absolute inset-0 flex items-center justify-center">
373-
{(status === "translating" || status === "uploading") && (
377+
{/* Status overlay */}
378+
{started && (
379+
<>
380+
{(status === "translating" || status === "uploading") && (
381+
<div className="absolute inset-0 flex items-center justify-center">
374382
<div className="rounded-full bg-background/80 p-2">
375383
<Spinner className="size-7 text-primary" />
376384
</div>
377-
)}
378-
{status === "done" && (
379-
<div className="absolute top-1 right-1 rounded-full bg-background/80 p-0.5">
380-
<CheckCircle size={20} weight="fill" className="text-green-500" />
381-
</div>
382-
)}
383-
{status === "failed" && (
385+
</div>
386+
)}
387+
{status === "done" && (
388+
<div className="absolute top-1 right-1 rounded-full bg-background/80 p-0.5">
389+
<CheckCircle size={20} weight="fill" className="text-green-500" />
390+
</div>
391+
)}
392+
{status === "failed" && (
393+
<div className="absolute inset-0 flex items-center justify-center">
384394
<div className="rounded-full bg-background/80 p-2" title={state?.error}>
385395
<XCircle size={24} weight="fill" className="text-destructive" />
386396
</div>
387-
)}
388-
</div>
389-
</div>
390-
);
391-
})}
392-
</div>
393-
</div>
394-
)}
395-
396-
{/* Idle state – preview of what will be translated */}
397-
{!started && (
398-
<div className="flex-1 overflow-y-auto">
399-
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
400-
{items.map((item) => {
401-
const token = item.screenshot.attributes.assetToken;
402-
return (
403-
<div
404-
key={item.screenshot.id}
405-
className="flex flex-col items-center rounded-lg border bg-muted/20 p-2"
406-
>
407-
{token ? (
408-
<img
409-
src={screenshotImageUrl(token, 400)}
410-
alt={item.screenshot.attributes.fileName}
411-
className="h-[300px] w-auto rounded object-contain"
412-
/>
413-
) : (
414-
<div className="flex h-[300px] w-[168px] items-center justify-center rounded bg-muted">
415-
<Spinner className="size-6 text-muted-foreground/40" />
416397
</div>
417398
)}
418-
</div>
419-
);
420-
})}
421-
</div>
422-
</div>
423-
)}
399+
</>
400+
)}
401+
</div>
402+
);
403+
})}
404+
</div>
424405

425406
{/* Gemini key input – shown when no key available */}
426407
{hasKey === false && !started && (
@@ -494,17 +475,24 @@ export function TranslateScreenshotsModal({
494475

495476
{started && (
496477
<>
497-
<div className="mr-auto text-xs text-muted-foreground">
498-
{inProgress
499-
? copyMode
500-
? "Uploading..."
501-
: "Translating... This can take 1\u20132 minutes per image."
502-
: allDone && !hasFailed
503-
? "All done!"
504-
: allDone && hasFailed
505-
? `${doneCount} completed, ${failedCount} failed`
506-
: "Stopped"
507-
}
478+
<div className="mr-auto flex items-center gap-3 text-xs text-muted-foreground">
479+
<span className="shrink-0">
480+
{inProgress
481+
? copyMode ? "Uploading..." : "Translating (up to 1\u20132 min per image)"
482+
: allDone && !hasFailed
483+
? "All done!"
484+
: allDone && hasFailed
485+
? `${failedCount} failed`
486+
: "Stopped"
487+
}
488+
</span>
489+
<div className="h-1.5 w-24 overflow-hidden rounded-full bg-muted">
490+
<div
491+
className="h-full rounded-full bg-primary transition-all duration-300"
492+
style={{ width: `${total > 0 ? ((doneCount + failedCount) / total) * 100 : 0}%` }}
493+
/>
494+
</div>
495+
<span className="shrink-0">{doneCount}/{total}</span>
508496
</div>
509497
{hasFailed && !inProgress && (
510498
<Button variant="outline" size="sm" className="gap-1" onClick={handleRetryFailed}>

0 commit comments

Comments
 (0)