Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions src/islands/dev/ClipboardInspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Button } from '@/components/ui/Button';
import { Alert } from '@/components/ui/Alert';
import {
readClipboard, parseDataTransfer, previewKindOf, formatSize, mimeToExtension,
readClipboard, parseDataTransfer, previewKindOf, formatSize, mimeToExtension, entryToBlob,

Check warning on line 6 in src/islands/dev/ClipboardInspector.tsx

View workflow job for this annotation

GitHub Actions / Test · Build · Lint

'previewKindOf' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 6 in src/islands/dev/ClipboardInspector.tsx

View workflow job for this annotation

GitHub Actions / Test · Build · Lint

'previewKindOf' is defined but never used. Allowed unused vars must match /^_/u
type ClipboardSnapshot, type ClipboardItemEntry, type PreviewKind,
} from '@/tools/dev/clipboard.lib';
import { downloadService } from '@/services/download.service';
Expand Down Expand Up @@ -54,19 +54,13 @@
function ItemPreview({ item }: { item: ClipboardItemEntry }) {
const [showHtml, setShowHtml] = useState<'source' | 'render'>('render');

function handleDownload() {
async function handleDownload() {
const ext = item.filename
? item.filename.split('.').pop() ?? mimeToExtension(item.type)
: mimeToExtension(item.type);
const name = item.filename ?? `clipboard.${ext}`;
if (item.blobUrl) {
downloadService.download(item.blobUrl, name);
} else if (item.text != null) {
const blob = new Blob([item.text], { type: item.type });
const url = URL.createObjectURL(blob);
downloadService.download(url, name);
setTimeout(() => URL.revokeObjectURL(url), 5000);
}
const blob = await entryToBlob(item);
if (blob) downloadService.download(blob, name);
}

return (
Expand Down Expand Up @@ -247,11 +241,11 @@
}
document.addEventListener('paste', onPaste);
return () => document.removeEventListener('paste', onPaste);
}, []);

Check warning on line 244 in src/islands/dev/ClipboardInspector.tsx

View workflow job for this annotation

GitHub Actions / Test · Build · Lint

React Hook useEffect has a missing dependency: 'addSnapshot'. Either include it or remove the dependency array

Check warning on line 244 in src/islands/dev/ClipboardInspector.tsx

View workflow job for this annotation

GitHub Actions / Test · Build · Lint

React Hook useEffect has a missing dependency: 'addSnapshot'. Either include it or remove the dependency array

useEffect(() => {
return () => {
blobUrls.current.forEach(u => URL.revokeObjectURL(u));

Check warning on line 248 in src/islands/dev/ClipboardInspector.tsx

View workflow job for this annotation

GitHub Actions / Test · Build · Lint

The ref value 'blobUrls.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'blobUrls.current' to a variable inside the effect, and use that variable in the cleanup function

Check warning on line 248 in src/islands/dev/ClipboardInspector.tsx

View workflow job for this annotation

GitHub Actions / Test · Build · Lint

The ref value 'blobUrls.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'blobUrls.current' to a variable inside the effect, and use that variable in the cleanup function
};
}, []);

Expand Down
26 changes: 24 additions & 2 deletions src/tools/dev/clipboard.lib.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import {
previewKindOf, formatSize, mimeToExtension, parseDataTransfer,
previewKindOf, formatSize, mimeToExtension, parseDataTransfer, entryToBlob,
} from './clipboard.lib';

// ─── entryToBlob (the download source — regression for the 66-byte PNG bug) ─────

describe('entryToBlob', () => {
it('wraps a text entry in a blob of the right type', async () => {
const blob = await entryToBlob({ type: 'text/plain', kind: 'text', text: 'hello', size: 5 });
expect(blob).toBeInstanceOf(Blob);
expect(await blob!.text()).toBe('hello');
expect(blob!.type).toBe('text/plain');
});
it('fetches the real blob for a binary entry (never the URL string)', async () => {
const png = new Blob([new Uint8Array([137, 80, 78, 71])], { type: 'image/png' });
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ blob: async () => png } as Response);
const blob = await entryToBlob({ type: 'image/png', kind: 'image', blobUrl: 'blob:mock', size: 4 });
expect(fetchSpy).toHaveBeenCalledWith('blob:mock');
expect(blob).toBe(png);
fetchSpy.mockRestore();
});
it('returns null when there is nothing to download', async () => {
expect(await entryToBlob({ type: 'text/plain', kind: 'text', size: 0 })).toBeNull();
});
});

// ─── previewKindOf ────────────────────────────────────────────────────────────

describe('previewKindOf', () => {
Expand Down
12 changes: 12 additions & 0 deletions src/tools/dev/clipboard.lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ export interface ClipboardSnapshot {
items: ClipboardItemEntry[];
}

/**
* Resolve the actual downloadable Blob for an entry. Binary entries only keep a
* `blobUrl` (an object-URL string), so we fetch the bytes back — passing the URL
* string straight to a download helper writes the ~60-char URL to disk instead of
* the file (the "saved PNG is 66 bytes / invalid" bug).
*/
export async function entryToBlob(item: ClipboardItemEntry): Promise<Blob | null> {
if (item.blobUrl) return fetch(item.blobUrl).then(r => r.blob()).catch(() => null);
if (item.text != null) return new Blob([item.text], { type: item.type });
return null;
}

// ─── Utilities ────────────────────────────────────────────────────────────────

export function previewKindOf(mimeType: string): PreviewKind {
Expand Down
Loading