Window drag-drop and paste share the same pick path as the file chooser; configure modal lives in the intake component.
182 lines
5.8 KiB
Vue
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>
|