Show ExifTool WASM download progress on upload queue rows.
Stream progress from the worker via axios, expose percent in the exiftool store, and format it on initializing metadata lines.
This commit is contained in:
@@ -14,11 +14,7 @@
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
<span
|
<span v-else class="h-10 w-10 shrink-0 rounded border border-border bg-muted/60" aria-hidden="true" />
|
||||||
v-else
|
|
||||||
class="h-10 w-10 shrink-0 rounded border border-border bg-muted/60"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div class="min-w-0 flex-1 space-y-1">
|
<div class="min-w-0 flex-1 space-y-1">
|
||||||
<div class="flex min-w-0 items-baseline justify-between gap-2">
|
<div class="flex min-w-0 items-baseline justify-between gap-2">
|
||||||
<span class="min-w-0 truncate text-sm font-medium">{{ row.file.name }}</span>
|
<span class="min-w-0 truncate text-sm font-medium">{{ row.file.name }}</span>
|
||||||
@@ -28,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex min-w-0 flex-col gap-0.5 text-xs">
|
<div class="flex min-w-0 flex-col gap-0.5 text-xs">
|
||||||
<span class="min-w-0 truncate" :class="metadataStatusClass(row.metadata)">
|
<span class="min-w-0 truncate" :class="metadataStatusClass(row.metadata)">
|
||||||
{{ row.metadata.message }}
|
{{ formatMetadataPreviewMessage(row.metadata) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-0 truncate" :class="uploadStatusClass(row.upload)">
|
<span class="min-w-0 truncate" :class="uploadStatusClass(row.upload)">
|
||||||
{{ row.upload.message }}
|
{{ row.upload.message }}
|
||||||
@@ -41,13 +37,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
import { type PropType } from 'vue';
|
import { type PropType } from 'vue';
|
||||||
|
import { useExiftoolStore } from '@/stores/exiftool';
|
||||||
import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview';
|
import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview';
|
||||||
import { makeKeyFromFile } from '@/utils/file/makeKeyFromFile';
|
import { makeKeyFromFile } from '@/utils/file/makeKeyFromFile';
|
||||||
import type { UploadQueueEntry } from '@/types/upload/UploadQueueEntry';
|
import type { UploadQueueEntry } from '@/types/upload/UploadQueueEntry';
|
||||||
import type { UploadQueueRow } from '@/types/upload/UploadQueueRow';
|
import type { UploadQueueRow } from '@/types/upload/UploadQueueRow';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
|
||||||
|
const exiftoolStore = useExiftoolStore();
|
||||||
|
|
||||||
|
const { wasmDownloadPercent } = storeToRefs(exiftoolStore);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rows: {
|
rows: {
|
||||||
type: Array as PropType<UploadQueueRow[]>,
|
type: Array as PropType<UploadQueueRow[]>,
|
||||||
@@ -89,4 +91,18 @@ const uploadStatusClass = (entry: UploadQueueEntry): string => {
|
|||||||
|
|
||||||
return 'text-muted-foreground';
|
return 'text-muted-foreground';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatMetadataPreviewMessage = (entry: MetadataPreviewEntry): string => {
|
||||||
|
if (entry.status === 'initializing') {
|
||||||
|
const percent = wasmDownloadPercent.value;
|
||||||
|
|
||||||
|
if (percent === null) {
|
||||||
|
return 'Downloading ExifTool…';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Downloading ExifTool… ${percent}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.message;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { ExiftoolWorkerOutMessage } from '@/types/workers/exiftoolWorkerOut
|
|||||||
export const useExiftoolStore = defineStore('exiftool', () => {
|
export const useExiftoolStore = defineStore('exiftool', () => {
|
||||||
const initStatus = ref<ExiftoolInitStatus>('idle');
|
const initStatus = ref<ExiftoolInitStatus>('idle');
|
||||||
const initErrorMessage = ref<string | null>(null);
|
const initErrorMessage = ref<string | null>(null);
|
||||||
|
const wasmDownloadPercent = ref<number | null>(null);
|
||||||
|
|
||||||
const worker = ref<Worker | null>(null);
|
const worker = ref<Worker | null>(null);
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ export const useExiftoolStore = defineStore('exiftool', () => {
|
|||||||
if (initStatus.value === 'idle') {
|
if (initStatus.value === 'idle') {
|
||||||
initStatus.value = 'loading';
|
initStatus.value = 'loading';
|
||||||
initErrorMessage.value = null;
|
initErrorMessage.value = null;
|
||||||
|
wasmDownloadPercent.value = 0;
|
||||||
initPromise = new Promise<void>((resolve, reject) => {
|
initPromise = new Promise<void>((resolve, reject) => {
|
||||||
initPromiseResolve = resolve;
|
initPromiseResolve = resolve;
|
||||||
initPromiseReject = reject;
|
initPromiseReject = reject;
|
||||||
@@ -69,8 +71,15 @@ export const useExiftoolStore = defineStore('exiftool', () => {
|
|||||||
const handleWorkerMessage = (event: MessageEvent<ExiftoolWorkerOutMessage>): void => {
|
const handleWorkerMessage = (event: MessageEvent<ExiftoolWorkerOutMessage>): void => {
|
||||||
const message = event.data;
|
const message = event.data;
|
||||||
|
|
||||||
|
if (message.type === 'wasm-download-progress') {
|
||||||
|
wasmDownloadPercent.value = message.percent;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === 'init-done') {
|
if (message.type === 'init-done') {
|
||||||
initStatus.value = 'ready';
|
initStatus.value = 'ready';
|
||||||
|
wasmDownloadPercent.value = null;
|
||||||
initPromiseResolve?.();
|
initPromiseResolve?.();
|
||||||
initPromiseResolve = null;
|
initPromiseResolve = null;
|
||||||
initPromiseReject = null;
|
initPromiseReject = null;
|
||||||
@@ -81,6 +90,7 @@ export const useExiftoolStore = defineStore('exiftool', () => {
|
|||||||
if (message.type === 'init-error') {
|
if (message.type === 'init-error') {
|
||||||
console.error('[exiftool] init-error', message.message);
|
console.error('[exiftool] init-error', message.message);
|
||||||
initStatus.value = 'error';
|
initStatus.value = 'error';
|
||||||
|
wasmDownloadPercent.value = null;
|
||||||
initErrorMessage.value = message.message;
|
initErrorMessage.value = message.message;
|
||||||
initPromiseReject?.(new Error(message.message));
|
initPromiseReject?.(new Error(message.message));
|
||||||
initPromiseResolve = null;
|
initPromiseResolve = null;
|
||||||
@@ -181,6 +191,7 @@ export const useExiftoolStore = defineStore('exiftool', () => {
|
|||||||
return {
|
return {
|
||||||
initStatus,
|
initStatus,
|
||||||
initErrorMessage,
|
initErrorMessage,
|
||||||
|
wasmDownloadPercent,
|
||||||
initializeExiftool,
|
initializeExiftool,
|
||||||
readMetadata,
|
readMetadata,
|
||||||
stripAllMetadata
|
stripAllMetadata
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview';
|
|||||||
import { makeKeyFromFile } from '@/utils/file/makeKeyFromFile';
|
import { makeKeyFromFile } from '@/utils/file/makeKeyFromFile';
|
||||||
|
|
||||||
const METADATA_IDLE_MESSAGE = 'Waiting to load metadata...';
|
const METADATA_IDLE_MESSAGE = 'Waiting to load metadata...';
|
||||||
const EXIFTOOL_INITIALIZING_MESSAGE = 'Initializing ExifTool. It may take a minute on slow networks...';
|
const EXIFTOOL_INITIALIZING_MESSAGE = 'Downloading ExifTool…';
|
||||||
const METADATA_PROCESSING_MESSAGE = 'Stripping metadata...';
|
const METADATA_PROCESSING_MESSAGE = 'Stripping metadata...';
|
||||||
const METADATA_READY_MESSAGE = 'Metadata stripped';
|
const METADATA_READY_MESSAGE = 'Metadata stripped';
|
||||||
const METADATA_LOAD_ERROR_MESSAGE = 'Could not load image metadata';
|
const METADATA_LOAD_ERROR_MESSAGE = 'Could not load image metadata';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export type ExiftoolWorkerOutMessage =
|
export type ExiftoolWorkerOutMessage =
|
||||||
|
| { type: 'wasm-download-progress'; percent: number }
|
||||||
| { type: 'init-done' }
|
| { type: 'init-done' }
|
||||||
| { type: 'init-error'; message: string }
|
| { type: 'init-error'; message: string }
|
||||||
| { type: 'read-metadata-done'; id: string; output: string }
|
| { type: 'read-metadata-done'; id: string; output: string }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import zeroperlWasmUrl from '@6over3/zeroperl-ts/zeroperl.wasm?url';
|
import zeroperlWasmUrl from '@6over3/zeroperl-ts/zeroperl.wasm?url';
|
||||||
import { parseMetadata, writeMetadata } from '@uswriting/exiftool';
|
import { parseMetadata, writeMetadata } from '@uswriting/exiftool';
|
||||||
|
import axios from 'axios';
|
||||||
import { EXIFTOOL_WORKER_INIT_JPEG } from '@/consts/exiftoolWorkerInitJpeg';
|
import { EXIFTOOL_WORKER_INIT_JPEG } from '@/consts/exiftoolWorkerInitJpeg';
|
||||||
import type { ExiftoolWorkerInMessage } from '@/types/workers/exiftoolWorkerInMessage';
|
import type { ExiftoolWorkerInMessage } from '@/types/workers/exiftoolWorkerInMessage';
|
||||||
import type { ExiftoolWorkerJob } from '@/types/workers/exiftoolWorkerJob';
|
import type { ExiftoolWorkerJob } from '@/types/workers/exiftoolWorkerJob';
|
||||||
@@ -14,13 +15,22 @@ let cachedZeroperlWasm: ArrayBuffer | null = null;
|
|||||||
*/
|
*/
|
||||||
const fetchZeroperlWasm = async (): Promise<Response> => {
|
const fetchZeroperlWasm = async (): Promise<Response> => {
|
||||||
if (!cachedZeroperlWasm) {
|
if (!cachedZeroperlWasm) {
|
||||||
const response = await fetch(zeroperlWasmUrl);
|
const { data } = await axios.get<ArrayBuffer>(zeroperlWasmUrl, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
onDownloadProgress: progressEvent => {
|
||||||
|
if (!progressEvent.total) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
const percent = Math.min(100, (progressEvent.loaded / progressEvent.total) * 100);
|
||||||
throw new Error(`Failed to load zeroperl.wasm: HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedZeroperlWasm = await response.arrayBuffer();
|
const rounded = parseFloat(percent.toFixed(2));
|
||||||
|
|
||||||
|
self.postMessage({ type: 'wasm-download-progress', percent: rounded });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cachedZeroperlWasm = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(cachedZeroperlWasm, {
|
return new Response(cachedZeroperlWasm, {
|
||||||
|
|||||||
Reference in New Issue
Block a user