diff --git a/frontend/src/stores/metadata.ts b/frontend/src/stores/metadata.ts new file mode 100644 index 0000000..d9dfbed --- /dev/null +++ b/frontend/src/stores/metadata.ts @@ -0,0 +1,83 @@ +import { parseMetadata } from '@uswriting/exiftool'; +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview'; +import { resolveErrorMessage } from '@/utils/resolveErrorMessage'; + +export const useMetadataStore = defineStore('metadata', () => { + const previewsByKey = ref>({}); + + const readExifToolStdout = async (file: File): Promise => { + const result = await parseMetadata(file); + + if (!result.success) { + throw new Error(result.error); + } + + return result.data; + }; + + const makeKeyFromFile = (file: File): string => `${file.name}\0${file.size}\0${file.lastModified}`; + + const getPreview = (file: File): MetadataPreviewEntry => { + const key = makeKeyFromFile(file); + + return previewsByKey.value[key] ?? { status: 'idle' }; + }; + + const loadPreview = async (file: File): Promise => { + const key = makeKeyFromFile(file); + + previewsByKey.value = { ...previewsByKey.value, [key]: { status: 'loading' } }; + + try { + const output = await readExifToolStdout(file); + + const entry: MetadataPreviewEntry = { status: 'ready', output }; + + previewsByKey.value = { ...previewsByKey.value, [key]: entry }; + + return entry; + } catch (error) { + const entry: MetadataPreviewEntry = { + status: 'error', + message: resolveErrorMessage(error, 'Could not read image metadata') + }; + + previewsByKey.value = { ...previewsByKey.value, [key]: entry }; + + return entry; + } + }; + + const loadPreviews = async (files: readonly File[]): Promise => { + await 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 = {}; + }; + + return { + previewsByKey, + makeKeyFromFile, + getPreview, + loadPreview, + loadPreviews, + clearPreview, + clearAllPreviews + }; +}); diff --git a/frontend/src/types/metadata/MetadataPreview.ts b/frontend/src/types/metadata/MetadataPreview.ts new file mode 100644 index 0000000..9462b8b --- /dev/null +++ b/frontend/src/types/metadata/MetadataPreview.ts @@ -0,0 +1,5 @@ +export type MetadataPreviewEntry = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'ready'; output: string } + | { status: 'error'; message: string }; diff --git a/frontend/src/utils/http/resolveAxiosErrorMessage.ts b/frontend/src/utils/http/resolveAxiosErrorMessage.ts index 6747a60..86ddee7 100644 --- a/frontend/src/utils/http/resolveAxiosErrorMessage.ts +++ b/frontend/src/utils/http/resolveAxiosErrorMessage.ts @@ -1,23 +1,40 @@ -import axios from 'axios'; +import { isAxiosError } from 'axios'; +import { resolveErrorMessage } from '@/utils/resolveErrorMessage'; -export function resolveAxiosErrorMessage(error: unknown, fallback = 'Request failed'): string { - if (axios.isAxiosError(error)) { - const data = error.response?.data; - if (typeof data === 'object' && data !== null && 'message' in data) { - const message = (data as { message: unknown }).message; - if (typeof message === 'string') { - return message; - } - if (Array.isArray(message)) { - return message.map(String).join(', '); - } +const getAxiosErrorResponseMessage = (data: unknown): string | null => { + if (typeof data === 'string') { + return data; + } + + if (typeof data !== 'object' || data === null || !('message' in data)) { + return null; + } + + const { message } = data; + + if (typeof message === 'string') { + return message; + } + + if (Array.isArray(message)) { + const texts = message.filter((item): item is string => typeof item === 'string'); + + if (texts.length === 1) { + return texts[0]; } - if (error.message) { - return error.message; + + if (texts.length > 1) { + return texts.join(', '); } } - if (error instanceof Error) { - return error.message; + + return null; +}; + +export const resolveAxiosErrorMessage = (error: unknown, fallback: string): string => { + if (!isAxiosError(error) || !error.response) { + return resolveErrorMessage(error, fallback); } - return fallback; -} + + return getAxiosErrorResponseMessage(error.response.data) ?? fallback; +}; diff --git a/frontend/src/utils/resolveErrorMessage.ts b/frontend/src/utils/resolveErrorMessage.ts new file mode 100644 index 0000000..60a375f --- /dev/null +++ b/frontend/src/utils/resolveErrorMessage.ts @@ -0,0 +1,7 @@ +export function resolveErrorMessage(error: unknown, fallback = 'Something went wrong'): string { + if (error instanceof Error && error.message.length > 0) { + return error.message; + } + + return fallback; +}