From 18597f4127f6c2a1a5a2ef73607fe91cffa30bf8 Mon Sep 17 00:00:00 2001 From: nobswebdev Date: Sat, 17 Oct 2026 21:39:59 +0200 Subject: [PATCH] Add upload queue UI with ExifTool metadata preview on pick. Users can select images within configured limits, see per-row load status, and inspect raw ExifTool output before upload work continues. --- frontend/package-lock.json | 13 ++ frontend/package.json | 1 + .../components/upload/MetadataInspector.vue | 40 ++++ frontend/src/stores/metadata.ts | 50 +++-- .../src/types/metadata/MetadataPreview.ts | 4 +- .../src/types/upload/PickedUploadAnalysis.ts | 6 + frontend/src/views/UploadView.vue | 188 +++++++++++++++++- frontend/src/workers/exiftoolWorker.ts | 4 +- 8 files changed, 274 insertions(+), 32 deletions(-) create mode 100644 frontend/src/components/upload/MetadataInspector.vue create mode 100644 frontend/src/types/upload/PickedUploadAnalysis.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 39e1685..2075cd9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@uswriting/exiftool": "1.0.9", "axios": "^1.20.0", "pinia": "^4.0.3", + "pretty-bytes": "^7.2.0", "tailwindcss": "^4.3.3", "vue": "^3.5.32", "vue-router": "^4.6.4" @@ -1924,6 +1925,18 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-bytes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.2.0.tgz", + "integrity": "sha512-T2kroepGB5DRVbkN/nOpW03+VRWqdtJhZNuCW3wYk3chdPjgi4GcTBOaxWfHq1MIM0ypidxsSWbQaAiRpxnXmg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 297625e..246539e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@uswriting/exiftool": "1.0.9", "axios": "^1.20.0", "pinia": "^4.0.3", + "pretty-bytes": "^7.2.0", "tailwindcss": "^4.3.3", "vue": "^3.5.32", "vue-router": "^4.6.4" diff --git a/frontend/src/components/upload/MetadataInspector.vue b/frontend/src/components/upload/MetadataInspector.vue new file mode 100644 index 0000000..b23e788 --- /dev/null +++ b/frontend/src/components/upload/MetadataInspector.vue @@ -0,0 +1,40 @@ + + + diff --git a/frontend/src/stores/metadata.ts b/frontend/src/stores/metadata.ts index 2ca494e..e8bbb8f 100644 --- a/frontend/src/stores/metadata.ts +++ b/frontend/src/stores/metadata.ts @@ -5,8 +5,20 @@ import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview'; const EXIFTOOL_PREPARE_ERROR_MESSAGE = 'Could not prepare ExifTool'; const EXIFTOOL_INITIALIZING_MESSAGE = 'Initializing ExifTool'; -const METADATA_READING_MESSAGE = 'Reading metadata'; -const METADATA_READ_ERROR_MESSAGE = 'Could not read image metadata'; +const METADATA_LOADING_MESSAGE = 'Loading metadata'; +const METADATA_LOAD_ERROR_MESSAGE = 'Could not load image metadata'; +const METADATA_IDLE_MESSAGE = 'Waiting to load metadata'; +const METADATA_READY_MESSAGE = 'Metadata loaded'; + +const METADATA_IDLE_PREVIEW: MetadataPreviewEntry = { + status: 'idle', + message: METADATA_IDLE_MESSAGE +}; + +const METADATA_LOADING_PREVIEW: MetadataPreviewEntry = { + status: 'loading', + message: METADATA_LOADING_MESSAGE +}; export const useMetadataStore = defineStore('metadata', () => { const previewsByKey = ref>({}); @@ -18,10 +30,14 @@ export const useMetadataStore = defineStore('metadata', () => { const makeKeyFromFile = (file: File): string => `${file.name}\0${file.size}\0${file.lastModified}`; - const getPreview = (file: File): MetadataPreviewEntry => { + const getPreview = (file?: File | null): MetadataPreviewEntry => { + if (!file) { + return METADATA_IDLE_PREVIEW; + } + const key = makeKeyFromFile(file); - return previewsByKey.value[key] ?? { status: 'idle' }; + return previewsByKey.value[key] ?? METADATA_IDLE_PREVIEW; }; const loadPreview = async (file: File): Promise => { @@ -41,7 +57,7 @@ export const useMetadataStore = defineStore('metadata', () => { if (exiftoolInitStatus.value === 'ready') { previewsByKey.value = { ...previewsByKey.value, - [key]: { status: 'loading', message: METADATA_READING_MESSAGE } + [key]: METADATA_LOADING_PREVIEW }; } else { previewsByKey.value = { @@ -54,7 +70,7 @@ export const useMetadataStore = defineStore('metadata', () => { previewsByKey.value = { ...previewsByKey.value, - [key]: { status: 'loading', message: METADATA_READING_MESSAGE } + [key]: METADATA_LOADING_PREVIEW }; } catch { const entry: MetadataPreviewEntry = { @@ -71,7 +87,11 @@ export const useMetadataStore = defineStore('metadata', () => { try { const output = await readMetadata(file); - const entry: MetadataPreviewEntry = { status: 'ready', output }; + const entry: MetadataPreviewEntry = { + status: 'ready', + message: METADATA_READY_MESSAGE, + output + }; previewsByKey.value = { ...previewsByKey.value, [key]: entry }; @@ -79,7 +99,7 @@ export const useMetadataStore = defineStore('metadata', () => { } catch { const entry: MetadataPreviewEntry = { status: 'error', - message: METADATA_READ_ERROR_MESSAGE + message: METADATA_LOAD_ERROR_MESSAGE }; previewsByKey.value = { ...previewsByKey.value, [key]: entry }; @@ -91,19 +111,6 @@ export const useMetadataStore = defineStore('metadata', () => { const loadPreviews = (files: readonly File[]): Promise => Promise.all(files.map(file => loadPreview(file))); - const clearPreview = (file: File): void => { - const key = makeKeyFromFile(file); - - if (!(key in previewsByKey.value)) { - return; - } - - const next = { ...previewsByKey.value }; - - delete next[key]; - previewsByKey.value = next; - }; - const clearAllPreviews = (): void => { previewsByKey.value = {}; }; @@ -114,7 +121,6 @@ export const useMetadataStore = defineStore('metadata', () => { getPreview, loadPreview, loadPreviews, - clearPreview, clearAllPreviews }; }); diff --git a/frontend/src/types/metadata/MetadataPreview.ts b/frontend/src/types/metadata/MetadataPreview.ts index d517470..35127c6 100644 --- a/frontend/src/types/metadata/MetadataPreview.ts +++ b/frontend/src/types/metadata/MetadataPreview.ts @@ -1,6 +1,6 @@ export type MetadataPreviewEntry = - | { status: 'idle' } + | { status: 'idle'; message: string } | { status: 'pending'; message: string } | { status: 'loading'; message: string } - | { status: 'ready'; output: string } + | { status: 'ready'; message: string; output: string } | { status: 'error'; message: string }; diff --git a/frontend/src/types/upload/PickedUploadAnalysis.ts b/frontend/src/types/upload/PickedUploadAnalysis.ts new file mode 100644 index 0000000..ad4539e --- /dev/null +++ b/frontend/src/types/upload/PickedUploadAnalysis.ts @@ -0,0 +1,6 @@ +export type PickedUploadAnalysis = { + accepted: File[]; + nonImageCount: number; + oversizedImagesCount: number; + queueOmittedImagesCount: number; +}; diff --git a/frontend/src/views/UploadView.vue b/frontend/src/views/UploadView.vue index a613591..849998b 100644 --- a/frontend/src/views/UploadView.vue +++ b/frontend/src/views/UploadView.vue @@ -1,19 +1,195 @@ diff --git a/frontend/src/workers/exiftoolWorker.ts b/frontend/src/workers/exiftoolWorker.ts index a383a04..7b0990f 100644 --- a/frontend/src/workers/exiftoolWorker.ts +++ b/frontend/src/workers/exiftoolWorker.ts @@ -93,7 +93,7 @@ const processJob = async (job: ExiftoolWorkerJob): Promise => { } catch (error) { self.postMessage({ type: 'init-error', - message: resolveErrorMessage(error, 'Failed to prepare ExifTool') + message: resolveErrorMessage(error, 'Could not prepare ExifTool') }); } @@ -117,7 +117,7 @@ const processJob = async (job: ExiftoolWorkerJob): Promise => { self.postMessage({ type: 'read-metadata-error', id: job.id, - message: resolveErrorMessage(error, 'Could not read image metadata') + message: resolveErrorMessage(error, 'Could not load image metadata') }); } }