diff --git a/docs/UPLOAD_PR_PLAN.md b/docs/UPLOAD_PR_PLAN.md new file mode 100644 index 0000000..ef8680b --- /dev/null +++ b/docs/UPLOAD_PR_PLAN.md @@ -0,0 +1,28 @@ +# Upload flow PR plan + +## Phase 1 — Configure UI (done) + +- [x] Split upload page: dropzone, queue, metadata inspector, configure modal +- [x] Batch settings form (ZK / hotlink, expiry, max views, ZK password) +- [x] Preset → API helpers (`resolveUploadBatchExpiresAt`, `resolveUploadBatchMaxViews`, ZK prepare options builder) +- [x] Modal validation on Upload; batch settings summary after confirm + +## Phase 2 — Upload execution + dual queue status (in progress) + +- [ ] **Separate upload state** from metadata (`UploadQueueEntry` per file key); metadata column unchanged in `UploadQueueList` +- [ ] Show upload column once a batch upload has started (pending / uploading / uploaded / failed / skipped) +- [ ] `onBatchUpload`: stripped blob → encrypt → `useUploadStore` POST (ZK + hotlink) +- [ ] Share URLs via `buildZkViewerPath` / `buildHotlinkImagePath` (+ optional absolute URL from `VITE_CLEARNET_DOMAIN`) +- [ ] Disable dropzone / configure while batch upload runs; clear upload state on new pick + +## Phase 3 — Hardening (follow-up or tail of PR) + +- [ ] ALTCHA: fetch challenge + solve, pass `x-altcha` on upload (frontend not wired yet; backend may allow when disabled) +- [ ] Copy-to-clipboard for share links; optional delete token display +- [ ] Modal Esc / backdrop close +- [ ] Single validation gate (modal vs builder) + +## Out of scope + +- Viewer / delete pages +- E2E tests against docker-compose diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 436616e..496fac1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@tailwindcss/vite": "^4.3.3", "@uswriting/exiftool": "1.0.9", "axios": "^1.20.0", + "dayjs": "^1.11.23", "hash-wasm": "^4.12.0", "pinia": "^4.0.3", "pretty-bytes": "^7.2.0", @@ -2199,6 +2200,12 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index f1bedbc..e5f88cb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@tailwindcss/vite": "^4.3.3", "@uswriting/exiftool": "1.0.9", "axios": "^1.20.0", + "dayjs": "^1.11.23", "hash-wasm": "^4.12.0", "pinia": "^4.0.3", "pretty-bytes": "^7.2.0", diff --git a/frontend/src/components.d.ts b/frontend/src/components.d.ts index f41ef2e..1d73c9e 100644 --- a/frontend/src/components.d.ts +++ b/frontend/src/components.d.ts @@ -14,7 +14,9 @@ declare module 'vue' { MetadataInspector: typeof import('./components/upload/MetadataInspector.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + UploadConfigureModal: typeof import('./components/upload/UploadConfigureModal.vue')['default'] UploadDropzone: typeof import('./components/upload/UploadDropzone.vue')['default'] UploadQueueList: typeof import('./components/upload/UploadQueueList.vue')['default'] + UploadSettingsPanel: typeof import('./components/upload/UploadSettingsPanel.vue')['default'] } } diff --git a/frontend/src/components/upload/MetadataInspector.vue b/frontend/src/components/upload/MetadataInspector.vue index 6c78537..a1ff4a9 100644 --- a/frontend/src/components/upload/MetadataInspector.vue +++ b/frontend/src/components/upload/MetadataInspector.vue @@ -1,7 +1,7 @@ @@ -28,8 +48,10 @@ import type UploadDropzone from '@/components/upload/UploadDropzone.vue'; import { config } from '@/config'; import { useExiftoolStore } from '@/stores/exiftool'; import { useMetadataStore } from '@/stores/metadata'; +import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm'; 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; @@ -45,6 +67,15 @@ const selectedFile = ref(null); const selectionError = ref(null); const loadingMetadata = ref(false); const dropzoneRef = ref | null>(null); +const batchSettings = ref({ + mode: 'zk', + expiryPreset: 'never', + maxViewsPreset: 'unlimited', + passwordEnabled: false, + password: '' +}); +const configureModalOpen = ref(false); +const confirmedBatchSummary = ref(null); onBeforeMount(async () => { try { @@ -66,6 +97,14 @@ const queueRows = computed(() => const selectedPreview = computed(() => getPreview(selectedFile.value)); +const canOpenConfigure = computed(() => { + if (queuedFiles.value.length === 0 || loadingMetadata.value) { + return false; + } + + return !queuedFiles.value.some(file => getPreview(file).status === 'error'); +}); + const onFilesPicked = async (picked: File[]): Promise => { if (loadingMetadata.value) { return; @@ -120,5 +159,12 @@ const resetUploadSelection = (): void => { queuedFiles.value = []; selectedFile.value = null; dropzoneRef.value?.clearInput(); + configureModalOpen.value = false; + confirmedBatchSummary.value = null; +}; + +const onBatchUpload = (): void => { + confirmedBatchSummary.value = formatUploadBatchSettingsSummary(batchSettings.value); + configureModalOpen.value = false; };