Slave/ui polishing #13
@@ -7,6 +7,18 @@
|
||||
:class="isRowSelected(row) ? 'bg-muted/80' : ''"
|
||||
@click="emit('select', row.file)"
|
||||
>
|
||||
<img
|
||||
v-if="row.thumbnailUrl"
|
||||
:src="row.thumbnailUrl"
|
||||
class="h-10 w-10 shrink-0 rounded border border-border object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="h-10 w-10 shrink-0 rounded border border-border bg-muted/60"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ row.file.name }}</span>
|
||||
<span class="shrink-0 text-xs text-muted-foreground">
|
||||
{{ prettyBytes(row.file.size, { maximumFractionDigits: 1 }) }}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { makeKeyFromFile } from '@/utils/file/makeKeyFromFile';
|
||||
|
||||
export const createQueueThumbnailUrls = (files: File[]): Record<string, string> => {
|
||||
const urlsByKey: Record<string, string> = {};
|
||||
|
||||
for (const file of files) {
|
||||
urlsByKey[makeKeyFromFile(file)] = URL.createObjectURL(file);
|
||||
}
|
||||
|
||||
return urlsByKey;
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export const revokeQueueThumbnailUrls = (urlsByKey: Record<string, string>): void => {
|
||||
for (const url of Object.values(urlsByKey)) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
@@ -28,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeMount, ref } from 'vue';
|
||||
import { computed, onBeforeMount, onBeforeUnmount, ref } from 'vue';
|
||||
import type UploadIntakeControls from '@/components/upload/UploadIntakeControls.vue';
|
||||
import { config } from '@/config';
|
||||
import { useExiftoolStore } from '@/stores/exiftool';
|
||||
@@ -39,6 +39,8 @@ import { makeKeyFromFile } from '@/utils/file/makeKeyFromFile';
|
||||
import { analyzePickedUploadFiles } from '@/utils/upload/analyzePickedUploadFiles';
|
||||
import { buildPickedUploadWarning } from '@/utils/upload/buildPickedUploadWarning';
|
||||
import { formatUploadBatchSettingsSummary } from '@/utils/upload/formatUploadBatchSettingsSummary';
|
||||
import { createQueueThumbnailUrls } from '@/utils/upload/createQueueThumbnailUrls';
|
||||
import { revokeQueueThumbnailUrls } from '@/utils/upload/revokeQueueThumbnailUrls';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
const { maxQueueSize, maxImageBytes } = config.validation.upload;
|
||||
@@ -67,6 +69,7 @@ const batchSettings = ref<UploadBatchSettingsForm>({
|
||||
const uploadConfirmed = ref(false);
|
||||
const showShareLinks = ref(false);
|
||||
const confirmedBatchSummary = ref<string | null>(null);
|
||||
const thumbnailUrlsByKey = ref<Record<string, string>>({});
|
||||
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
@@ -76,6 +79,10 @@ onBeforeMount(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resetUploadSession();
|
||||
});
|
||||
|
||||
const maxImageSizeFormatted = computed(() => prettyBytes(maxImageBytes, { binary: true, maximumFractionDigits: 1 }));
|
||||
|
||||
const queueRows = computed(() =>
|
||||
@@ -85,6 +92,7 @@ const queueRows = computed(() =>
|
||||
return {
|
||||
file,
|
||||
key,
|
||||
thumbnailUrl: thumbnailUrlsByKey.value[key] ?? null,
|
||||
metadata: metadataStore.getPreview(file),
|
||||
upload: uploadStore.getUpload(file)
|
||||
};
|
||||
@@ -136,6 +144,7 @@ const onFilesPicked = async (picked: File[]): Promise<void> => {
|
||||
);
|
||||
|
||||
queuedFiles.value = accepted;
|
||||
thumbnailUrlsByKey.value = createQueueThumbnailUrls(accepted);
|
||||
selectedFile.value = accepted[0] ?? null;
|
||||
|
||||
loadingMetadata.value = true;
|
||||
@@ -148,9 +157,10 @@ const onFilesPicked = async (picked: File[]): Promise<void> => {
|
||||
loadingMetadata.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetUploadSession = (): void => {
|
||||
selectionError.value = null;
|
||||
revokeQueueThumbnailUrls(thumbnailUrlsByKey.value);
|
||||
thumbnailUrlsByKey.value = {};
|
||||
clearAllPreviews();
|
||||
queuedFiles.value = [];
|
||||
selectedFile.value = null;
|
||||
|
||||
Reference in New Issue
Block a user