diff --git a/frontend/src/components/upload/UploadQueueList.vue b/frontend/src/components/upload/UploadQueueList.vue
index 49e9a24..083cb2c 100644
--- a/frontend/src/components/upload/UploadQueueList.vue
+++ b/frontend/src/components/upload/UploadQueueList.vue
@@ -7,6 +7,18 @@
:class="isRowSelected(row) ? 'bg-muted/80' : ''"
@click="emit('select', row.file)"
>
+
+
{{ row.file.name }}
{{ prettyBytes(row.file.size, { maximumFractionDigits: 1 }) }}
diff --git a/frontend/src/types/upload/UploadQueueRow.ts b/frontend/src/types/upload/UploadQueueRow.ts
index ee4452c..6c6b81c 100644
--- a/frontend/src/types/upload/UploadQueueRow.ts
+++ b/frontend/src/types/upload/UploadQueueRow.ts
@@ -4,6 +4,7 @@ import type { UploadQueueEntry } from '@/types/upload/UploadQueueEntry';
export type UploadQueueRow = {
file: File;
key: string;
+ thumbnailUrl: string | null;
metadata: MetadataPreviewEntry;
upload: UploadQueueEntry;
};
diff --git a/frontend/src/utils/upload/createQueueThumbnailUrls.spec.ts b/frontend/src/utils/upload/createQueueThumbnailUrls.spec.ts
new file mode 100644
index 0000000..bb9d443
--- /dev/null
+++ b/frontend/src/utils/upload/createQueueThumbnailUrls.spec.ts
@@ -0,0 +1,29 @@
+import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
+import { createQueueThumbnailUrls } from '@/utils/upload/createQueueThumbnailUrls';
+
+const makeFile = (name: string, size: number, lastModified: number): File =>
+ new File([new Uint8Array(size)], name, { type: 'image/png', lastModified });
+
+describe('createQueueThumbnailUrls', () => {
+ beforeEach(() => {
+ vi.stubGlobal(
+ 'URL',
+ Object.assign(globalThis.URL, {
+ createObjectURL: vi.fn(() => 'blob:mock-1')
+ })
+ );
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it('creates one object URL per file', () => {
+ const files = [makeFile('a.png', 10, 1), makeFile('b.png', 20, 2)];
+
+ const urlsByKey = createQueueThumbnailUrls(files);
+
+ expect(URL.createObjectURL).toHaveBeenCalledTimes(2);
+ expect(Object.keys(urlsByKey)).toHaveLength(2);
+ });
+});
diff --git a/frontend/src/utils/upload/createQueueThumbnailUrls.ts b/frontend/src/utils/upload/createQueueThumbnailUrls.ts
new file mode 100644
index 0000000..7e92d16
--- /dev/null
+++ b/frontend/src/utils/upload/createQueueThumbnailUrls.ts
@@ -0,0 +1,11 @@
+import { makeKeyFromFile } from '@/utils/file/makeKeyFromFile';
+
+export const createQueueThumbnailUrls = (files: File[]): Record => {
+ const urlsByKey: Record = {};
+
+ for (const file of files) {
+ urlsByKey[makeKeyFromFile(file)] = URL.createObjectURL(file);
+ }
+
+ return urlsByKey;
+};
diff --git a/frontend/src/utils/upload/revokeQueueThumbnailUrls.spec.ts b/frontend/src/utils/upload/revokeQueueThumbnailUrls.spec.ts
new file mode 100644
index 0000000..2e02d01
--- /dev/null
+++ b/frontend/src/utils/upload/revokeQueueThumbnailUrls.spec.ts
@@ -0,0 +1,20 @@
+import { describe, expect, it, vi } from 'vitest';
+import { revokeQueueThumbnailUrls } from '@/utils/upload/revokeQueueThumbnailUrls';
+
+describe('revokeQueueThumbnailUrls', () => {
+ it('revokes every URL in the map', () => {
+ vi.stubGlobal(
+ 'URL',
+ Object.assign(globalThis.URL, {
+ revokeObjectURL: vi.fn()
+ })
+ );
+
+ revokeQueueThumbnailUrls({ a: 'blob:1', b: 'blob:2' });
+
+ expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:1');
+ expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:2');
+
+ vi.unstubAllGlobals();
+ });
+});
diff --git a/frontend/src/utils/upload/revokeQueueThumbnailUrls.ts b/frontend/src/utils/upload/revokeQueueThumbnailUrls.ts
new file mode 100644
index 0000000..2325116
--- /dev/null
+++ b/frontend/src/utils/upload/revokeQueueThumbnailUrls.ts
@@ -0,0 +1,5 @@
+export const revokeQueueThumbnailUrls = (urlsByKey: Record): void => {
+ for (const url of Object.values(urlsByKey)) {
+ URL.revokeObjectURL(url);
+ }
+};
diff --git a/frontend/src/views/UploadView.vue b/frontend/src/views/UploadView.vue
index 7e48c85..9efcfae 100644
--- a/frontend/src/views/UploadView.vue
+++ b/frontend/src/views/UploadView.vue
@@ -28,7 +28,7 @@