fix(dsh): keep extreme screenshot thumbnails usable

This commit is contained in:
drakezhang
2026-09-23 13:09:31 +08:00
parent bbde2d73a6
commit 2b01a15400
3 changed files with 71 additions and 6 deletions
@@ -4,6 +4,17 @@
max-width: 100%;
}
.status,
.retry {
max-width: 100%;
overflow-wrap: anywhere;
text-align: center;
}
.status {
font-size: 12px;
}
.thumbnail,
.retry,
.preview button {
@@ -27,10 +38,12 @@
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.retry,
.retry {
padding: 6px 8px;
}
.preview button {
padding: 6px 10px;
}
@@ -4,6 +4,9 @@ import css from "./ScreenshotImage.module.css";
type ImageState = { status: "loading" } | { status: "error" } | { status: "ready"; url: string };
const MIN_EDGE = 60;
const MAX_EDGE = 240;
/** Plugin-owned presentation: DSH's attachment client no longer exports image components. */
export function ScreenshotImage({
attachment,
@@ -16,6 +19,13 @@ export function ScreenshotImage({
const [attempt, setAttempt] = useState(0);
const [open, setOpen] = useState(false);
const label = attachment.name ?? "screenshot";
const naturalRatio = attachment.width / attachment.height;
const ratio = Math.min(4, Math.max(0.25, naturalRatio));
// Keep both axes usable for loading/retry, even for tiny element captures.
const width = Math.max(
MIN_EDGE * Math.max(1, ratio),
Math.min(MAX_EDGE, MAX_EDGE * ratio, attachment.width, attachment.height * ratio),
);
useEffect(() => {
let active = true;
@@ -45,19 +55,23 @@ export function ScreenshotImage({
<div
className={css.frame}
style={{
width: Math.min(attachment.width, 240, (240 * attachment.width) / attachment.height),
aspectRatio: `${attachment.width} / ${attachment.height}`,
width,
aspectRatio: String(ratio),
}}
>
{image.status === "loading" ? (
<span role="status">Loading…</span>
<span role="status" className={css.status}>
Loading…
</span>
) : image.status === "error" ? (
<button
type="button"
className={css.retry}
aria-label="Load failed — retry"
title="Load failed — retry"
onClick={() => setAttempt((value) => value + 1)}
>
Load failed — retry
Retry
</button>
) : (
<button
@@ -72,6 +86,11 @@ export function ScreenshotImage({
alt={label}
width={attachment.width}
height={attachment.height}
style={{
objectFit: naturalRatio === ratio ? "scale-down" : "cover",
objectPosition:
naturalRatio < 0.25 ? "center top" : naturalRatio > 4 ? "left center" : "center",
}}
onError={() => setImage({ status: "error" })}
/>
</button>
@@ -19,6 +19,39 @@ afterEach(() => {
vi.restoreAllMocks();
});
it.each([
{ width: 1280, height: 40, boxWidth: 240, ratio: 4, fit: "cover", position: "left center" },
{ width: 300, height: 3000, boxWidth: 60, ratio: 0.25, fit: "cover", position: "center top" },
{ width: 1, height: 10000, boxWidth: 60, ratio: 0.25, fit: "cover", position: "center top" },
{ width: 16, height: 16, boxWidth: 60, ratio: 1, fit: "scale-down", position: "center" },
{ width: 128, height: 96, boxWidth: 128, ratio: 4 / 3, fit: "scale-down", position: "center" },
])("keeps a $width×$height capture usable without cropping its preview", async (size) => {
const imageAttachment = { ...attachment, width: size.width, height: size.height };
let finish!: (url: string) => void;
const load = vi.fn(
() =>
new Promise<string>((resolve) => {
finish = resolve;
}),
);
render(<ScreenshotImage attachment={imageAttachment} load={load} />);
const frame = screen.getByRole("status").parentElement!;
expect(frame.style.width).toBe(`${size.boxWidth}px`);
const [horizontal, vertical = 1] = frame.style.aspectRatio.split("/").map(Number);
expect(horizontal / vertical).toBe(size.ratio);
await act(async () => finish("blob:thumbnail"));
const thumbnail = screen.getByRole("img");
expect(thumbnail.parentElement?.parentElement).toBe(frame);
expect(thumbnail.style.objectFit).toBe(size.fit);
expect(thumbnail.style.objectPosition).toBe(size.position);
fireEvent.click(screen.getByRole("button", { name: "Open screenshot screenshot.png" }));
const preview = screen.getByRole("dialog").querySelector("img")!;
expect(preview.getAttribute("src")).toBe("blob:thumbnail");
expect(preview.style.objectFit).toBe("");
expect(preview.style.objectPosition).toBe("");
});
it("retries an attachment read failure and releases the loaded URL on unmount", async () => {
const load = vi.fn().mockRejectedValueOnce(new Error("offline")).mockResolvedValue("blob:retry");
const view = render(<ScreenshotImage attachment={attachment} load={load} />);