Add batch upload configure modal and settings mapping utilities.
Users configure share mode, expiry, and max views before upload; batch summary confirms choices above the queue.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="flex min-h-48 flex-col rounded-lg border border-border bg-card" aria-live="polite">
|
||||
<header class="border-b border-border px-4 py-3">
|
||||
<h2 class="text-sm font-medium text-card-foreground">Metadata</h2>
|
||||
<h2 class="text-sm font-medium text-card-foreground">Metadata Inspector</h2>
|
||||
<p v-if="fileName" class="mt-0.5 truncate text-xs text-muted-foreground">{{ fileName }}</p>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="open" class="fixed inset-0 z-50 flex items-end justify-center bg-black/50 p-4 sm:items-center">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="upload-configure-title"
|
||||
class="flex w-full max-w-lg flex-col rounded-lg border border-border bg-card shadow-lg"
|
||||
>
|
||||
<header class="border-b border-border px-4 py-3">
|
||||
<h2 id="upload-configure-title" class="text-base font-medium text-card-foreground">
|
||||
Configure upload
|
||||
</h2>
|
||||
<p class="mt-1 text-xs text-muted-foreground">Settings apply to all chosen images.</p>
|
||||
</header>
|
||||
|
||||
<div class="px-4 py-4">
|
||||
<UploadSettingsPanel v-model="settings" />
|
||||
<p v-if="uploadValidationError" class="mt-3 text-sm text-destructive">
|
||||
{{ uploadValidationError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<footer class="flex flex-col-reverse gap-2 border-t border-border px-4 py-3 sm:flex-row sm:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-border px-4 py-2 text-sm font-medium transition-colors hover:bg-muted/60"
|
||||
@click="close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:opacity-90"
|
||||
@click="onUploadClick"
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
|
||||
import { validateUploadBatchSettings } from '@/utils/upload/validateUploadBatchSettings';
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true });
|
||||
|
||||
const settings = defineModel<UploadBatchSettingsForm>('settings', { required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
upload: [];
|
||||
}>();
|
||||
|
||||
const uploadValidationError = ref<string | null>(null);
|
||||
|
||||
watch(open, isOpen => {
|
||||
if (!isOpen) {
|
||||
uploadValidationError.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
const close = (): void => {
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
const onUploadClick = (): void => {
|
||||
const error = validateUploadBatchSettings(settings.value);
|
||||
|
||||
if (error) {
|
||||
uploadValidationError.value = error;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
uploadValidationError.value = null;
|
||||
|
||||
emit('upload');
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-5">
|
||||
<fieldset class="space-y-3">
|
||||
<legend class="text-sm font-medium text-foreground">Share mode</legend>
|
||||
|
||||
<div class="space-y-3">
|
||||
<label
|
||||
class="flex cursor-pointer gap-3 rounded-md border border-border p-3 has-checked:border-primary has-checked:bg-muted/40"
|
||||
>
|
||||
<input v-model="settings.mode" class="mt-0.5" type="radio" name="upload-mode" value="zk" />
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="text-sm font-medium">Zero knowledge</span>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Images are E2E encrypted. The server has no technical ability to decrypt them. Having the link
|
||||
gives permission to view the image. You can additionally password-protect the upload so the
|
||||
link alone is not enough. Great for private sharing, but doesn't work with BBCode, HTML,
|
||||
etc.
|
||||
</p>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label
|
||||
class="flex cursor-pointer gap-3 rounded-md border border-border p-3 has-checked:border-primary has-checked:bg-muted/40"
|
||||
>
|
||||
<input v-model="settings.mode" class="mt-0.5" type="radio" name="upload-mode" value="hotlink" />
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="text-sm font-medium">Hotlink</span>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Still encrypted at rest, but the server decrypts on the fly. Works with BBCode, HTML,
|
||||
etc.
|
||||
Encryption here serves mainly as
|
||||
<span class="whitespace-nowrap" aria-describedby="crypto-shred-footnote">
|
||||
<span class="font-bold">crypto-shred</span
|
||||
><sup class="font-bold text-[0.7em] text-muted-foreground" aria-hidden="true"
|
||||
>1</sup
|
||||
> </span
|
||||
>, because hotlink images are intended to be public.
|
||||
</p>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p id="crypto-shred-footnote" class="text-xs italic leading-snug text-muted-foreground">
|
||||
<sup class="mr-1 font-bold not-italic text-[0.85em]">1</sup>
|
||||
Hard drives do not guarantee that deleted files cannot be recovered. Crypto-shred consists of storing
|
||||
data as encrypted blobs, so recovered leftovers are useless without the decryption key, which is never
|
||||
stored on the server.
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1.5 text-sm">
|
||||
<span class="font-medium">Expiry</span>
|
||||
<select
|
||||
v-model="settings.expiryPreset"
|
||||
class="rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
>
|
||||
<option v-for="option in expiryOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="flex flex-col gap-1.5 text-sm">
|
||||
<span class="font-medium">Max views</span>
|
||||
<select
|
||||
v-model="settings.maxViewsPreset"
|
||||
class="rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
>
|
||||
<option v-for="option in maxViewsOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="settings.mode === 'zk'" class="space-y-3 rounded-md border border-border p-3">
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<input v-model="settings.passwordEnabled" type="checkbox" class="rounded border-input" />
|
||||
<span class="font-medium">Set password</span>
|
||||
</label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Requires the link and this password to decrypt. Use when the link alone should not be enough.
|
||||
</p>
|
||||
|
||||
<template v-if="settings.passwordEnabled">
|
||||
<label class="flex flex-col gap-1.5 text-sm">
|
||||
<span>Password</span>
|
||||
<input
|
||||
v-model="settings.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { config } from '@/config';
|
||||
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
|
||||
import type { UploadExpiryPreset } from '@/types/upload/UploadExpiryPreset';
|
||||
import type { UploadMaxViewsPreset } from '@/types/upload/UploadMaxViewsPreset';
|
||||
|
||||
const settings = defineModel<UploadBatchSettingsForm>({ required: true });
|
||||
|
||||
const { maxViewsMax } = config.validation.upload;
|
||||
|
||||
const expiryOptions: { value: UploadExpiryPreset; label: string }[] = [
|
||||
{ value: 'never', label: 'Never' },
|
||||
{ value: '1h', label: '1 hour' },
|
||||
{ value: '24h', label: '24 hours' },
|
||||
{ value: '7d', label: '7 days' },
|
||||
{ value: '30d', label: '30 days' }
|
||||
];
|
||||
|
||||
const maxViewsOptions = computed(() => {
|
||||
const options: { value: UploadMaxViewsPreset; label: string; numeric: number | null }[] = [
|
||||
{ value: 'unlimited', label: 'Unlimited', numeric: null },
|
||||
{ value: '1', label: '1- Burn after read', numeric: 1 },
|
||||
{ value: '10', label: '10', numeric: 10 },
|
||||
{ value: '100', label: '100', numeric: 100 },
|
||||
{ value: '1000', label: '1000', numeric: 1000 },
|
||||
{ value: '10000', label: '10000', numeric: 10000 }
|
||||
];
|
||||
|
||||
return options.filter(option => option.numeric === null || option.numeric <= maxViewsMax);
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user