Files
private_image/frontend/src/views/UploadView.vue
T
nobswebdev 9cc73e0dc5 replace upload dropzone with global intake controls.
Window drag-drop and paste share the same pick path as the file chooser; configure modal lives in the intake component.
2026-10-29 21:40:07 +01:00

182 lines
5.8 KiB
Vue

<template>
<main class="mx-auto flex w-full max-w-3xl flex-1 flex-col gap-6 p-8">
<div class="flex flex-col gap-2">
<UploadIntakeControls
ref="intakeControlsRef"
v-model:settings="batchSettings"
:disabled="loadingMetadata || batchUploading"
:can-open-configure="canOpenConfigure"
:upload-confirmed="uploadConfirmed"
:metadata-error-count="metadataErrorCount"
@pick="onFilesPicked"
@upload="onBatchUpload"
/>
<p v-if="selectionError" class="text-sm text-destructive">{{ selectionError }}</p>
</div>
<p v-if="confirmedBatchSummary" class="text-center text-xs text-muted-foreground">
{{ confirmedBatchSummary }}
</p>
<UploadQueueList :rows="queueRows" :selected-file="selectedFile" @select="selectedFile = $event" />
<UploadShareResults v-if="showShareLinks" :file-name="selectedFile?.name ?? null" :upload="selectedUpload" />
<MetadataInspector :file-name="selectedFile?.name ?? null" :entry="selectedPreview" />
</main>
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref } from 'vue';
import type UploadIntakeControls from '@/components/upload/UploadIntakeControls.vue';
import { config } from '@/config';
import { useExiftoolStore } from '@/stores/exiftool';
import { useMetadataStore } from '@/stores/metadata';
import { useUploadStore } from '@/stores/upload';
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
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 prettyBytes from 'pretty-bytes';
const { maxQueueSize, maxImageBytes } = config.validation.upload;
const exiftoolStore = useExiftoolStore();
const metadataStore = useMetadataStore();
const uploadStore = useUploadStore();
const { initializeExiftool } = exiftoolStore;
const { loadPreviews, clearAllPreviews } = metadataStore;
const { clearAllUploads, uploadBatch } = uploadStore;
const queuedFiles = ref<File[]>([]);
const selectedFile = ref<File | null>(null);
const selectionError = ref<string | null>(null);
const loadingMetadata = ref(false);
const batchUploading = ref(false);
const intakeControlsRef = ref<InstanceType<typeof UploadIntakeControls> | null>(null);
const batchSettings = ref<UploadBatchSettingsForm>({
mode: 'zk',
expiryPreset: 'never',
maxViewsPreset: 'unlimited',
passwordEnabled: false,
password: ''
});
const uploadConfirmed = ref(false);
const showShareLinks = ref(false);
const confirmedBatchSummary = ref<string | null>(null);
onBeforeMount(async () => {
try {
await initializeExiftool();
} catch (error) {
console.error(error);
}
});
const maxImageSizeFormatted = computed(() => prettyBytes(maxImageBytes, { binary: true, maximumFractionDigits: 1 }));
const queueRows = computed(() =>
queuedFiles.value.map(file => {
const key = makeKeyFromFile(file);
return {
file,
key,
metadata: metadataStore.getPreview(file),
upload: uploadStore.getUpload(file)
};
})
);
const selectedPreview = computed(() => metadataStore.getPreview(selectedFile.value));
const selectedUpload = computed(() => uploadStore.getUpload(selectedFile.value));
const metadataErrorCount = computed(
() => queuedFiles.value.filter(file => metadataStore.getPreview(file).status === 'error').length
);
const canOpenConfigure = computed(() => queuedFiles.value.length > 0 && !loadingMetadata.value);
const onFilesPicked = async (picked: File[]): Promise<void> => {
if (loadingMetadata.value || batchUploading.value) {
return;
}
resetUploadSession();
if (picked.length === 0) {
return;
}
const { accepted, nonImageCount, oversizedImagesCount, queueOmittedImagesCount } = analyzePickedUploadFiles(
picked,
maxImageBytes,
maxQueueSize
);
if (nonImageCount === picked.length) {
selectionError.value = 'Only image files can be added.';
return;
}
if (accepted.length === 0) {
selectionError.value = `Each image must be at most ${maxImageSizeFormatted.value}.`;
return;
}
selectionError.value = buildPickedUploadWarning(
{ nonImageCount, oversizedImagesCount, queueOmittedImagesCount },
maxQueueSize
);
queuedFiles.value = accepted;
selectedFile.value = accepted[0] ?? null;
loadingMetadata.value = true;
try {
await loadPreviews(accepted);
} catch (error) {
console.error(error);
} finally {
loadingMetadata.value = false;
}
};
const resetUploadSession = (): void => {
selectionError.value = null;
clearAllPreviews();
queuedFiles.value = [];
selectedFile.value = null;
clearAllUploads();
intakeControlsRef.value?.clearInput();
intakeControlsRef.value?.closeConfigureModal();
uploadConfirmed.value = false;
showShareLinks.value = false;
confirmedBatchSummary.value = null;
};
const onBatchUpload = async (uploadDespiteMetadataFailure?: boolean): Promise<void> => {
const settings = batchSettings.value;
confirmedBatchSummary.value = formatUploadBatchSettingsSummary(settings);
uploadConfirmed.value = true;
batchUploading.value = true;
try {
await uploadBatch(queuedFiles.value, settings, uploadDespiteMetadataFailure);
} catch (error) {
console.error(error);
} finally {
batchUploading.value = false;
showShareLinks.value = true;
}
};
</script>