同步设置
This commit is contained in:
1 parent
832261739f
commit
92aaf6fbb8
28 files changed
+705
-227
No files matched your search
+24
-1
@@ -7,22 +7,44 @@
|
||||
|
||||
.upload-card-flip-inner {
|
||||
position: relative;
|
||||
transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition:
|
||||
transform 0.55s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
min-height 0.35s ease;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.upload-card-flip.is-flipped .upload-card-flip-inner {
|
||||
min-height: min(40rem, calc(100vh - 11rem));
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.upload-card-flip--file.is-flipped .upload-card-flip-inner {
|
||||
min-height: min(27rem, calc(100vh - 11rem));
|
||||
}
|
||||
|
||||
.upload-card-flip.is-flipped .upload-card-face--front {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.upload-card-flip.is-flipped .upload-settings-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.upload-card-face {
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.upload-card-face--front {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.upload-card-face--back {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
@@ -33,6 +55,7 @@
|
||||
|
||||
.upload-card-flip.is-flipped .upload-card-flip-inner {
|
||||
transform: none;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.upload-card-flip .upload-card-face--back {
|
||||
|
||||
@@ -74,16 +74,7 @@ function fileIcon(item: FileItem): string {
|
||||
<p class="truncate text-xs text-muted">
|
||||
{{ uploadedLabel }} · {{ formatSize(item.size) }}
|
||||
</p>
|
||||
<div
|
||||
v-if="item.tags?.length"
|
||||
class="flex flex-wrap gap-1"
|
||||
>
|
||||
<TagBadge
|
||||
v-for="tag in item.tags"
|
||||
:key="tag.id"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
<TagBadgeList :tags="item.tags" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-around border-t border-default px-2 py-2">
|
||||
|
||||
@@ -633,20 +633,11 @@ defineExpose({ refresh })
|
||||
{{ uploadedLabel(item) }}
|
||||
</td>
|
||||
<td class="hidden px-3 py-2 md:table-cell">
|
||||
<div
|
||||
v-if="item.tags?.length"
|
||||
class="flex flex-wrap gap-1"
|
||||
>
|
||||
<TagBadge
|
||||
v-for="tag in item.tags"
|
||||
:key="tag.id"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
class="text-muted"
|
||||
>{{ t('tags.unlabeled') }}</span>
|
||||
<TagBadgeList
|
||||
:tags="item.tags"
|
||||
compact
|
||||
:empty-label="t('tags.unlabeled')"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
|
||||
@@ -32,24 +32,28 @@ onMounted(() => {
|
||||
const text = computed(() => {
|
||||
const allowedVideos = uploadPolicy.value?.allowedVideoExtensions ?? []
|
||||
const forbiddenFiles = uploadPolicy.value?.forbiddenFileExtensions ?? []
|
||||
const videoMaxFileSizeMb = uploadPolicy.value?.videoMaxFileSizeMb ?? 500
|
||||
const fileMaxFileSizeMb = uploadPolicy.value?.fileMaxFileSizeMb ?? 100
|
||||
return props.kind === 'video'
|
||||
? {
|
||||
uploadTitle: t('library.video.uploadTitle'),
|
||||
uploadHint: t('library.video.uploadHint'),
|
||||
uploadFormats: allowedVideos.length
|
||||
? t('library.video.uploadFormatsAllowed', {
|
||||
formats: allowedVideos.map(item => item.toUpperCase()).join(' / ')
|
||||
formats: allowedVideos.map(item => item.toUpperCase()).join(' / '),
|
||||
size: videoMaxFileSizeMb
|
||||
})
|
||||
: t('library.video.uploadFormats')
|
||||
: t('library.video.uploadFormats', { size: videoMaxFileSizeMb })
|
||||
}
|
||||
: {
|
||||
uploadTitle: t('library.file.uploadTitle'),
|
||||
uploadHint: t('library.file.uploadHint'),
|
||||
uploadFormats: forbiddenFiles.length
|
||||
? t('library.file.uploadFormatsBlocked', {
|
||||
formats: forbiddenFiles.map(item => item.toUpperCase()).join(' / ')
|
||||
formats: forbiddenFiles.map(item => item.toUpperCase()).join(' / '),
|
||||
size: fileMaxFileSizeMb
|
||||
})
|
||||
: t('library.file.uploadFormatsUnrestricted')
|
||||
: t('library.file.uploadFormatsUnrestricted', { size: fileMaxFileSizeMb })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const emit = defineEmits<{
|
||||
'created': [tag: ImageTag]
|
||||
}>()
|
||||
|
||||
const COMMON_LIMIT = 12
|
||||
const COMMON_LIMIT = 10
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const { createTag } = useTags()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import type { ImageTag } from '~/types/image'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
tags?: ImageTag[]
|
||||
max?: number
|
||||
compact?: boolean
|
||||
emptyLabel?: string
|
||||
}>(), {
|
||||
tags: () => [],
|
||||
max: 3,
|
||||
compact: false,
|
||||
emptyLabel: ''
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'tag-click': [tagId: number]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const visibleTags = computed(() => props.tags.slice(0, props.max))
|
||||
const hiddenTags = computed(() => props.tags.slice(props.max))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="tags.length"
|
||||
class="flex flex-wrap items-center"
|
||||
:class="compact ? 'gap-1' : 'gap-1.5'"
|
||||
>
|
||||
<TagBadge
|
||||
v-for="tag in visibleTags"
|
||||
:key="tag.id"
|
||||
:tag="tag"
|
||||
@click="emit('tag-click', tag.id)"
|
||||
/>
|
||||
<UPopover
|
||||
v-if="hiddenTags.length"
|
||||
:content="{ side: 'bottom', align: 'start' }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex rounded-md border border-default text-muted transition-colors hover:border-primary/40 hover:text-primary"
|
||||
:class="compact ? 'px-1.5 py-0.5 text-[11px]' : 'px-2 py-0.5 text-xs'"
|
||||
:aria-label="t('tags.moreTags', { n: hiddenTags.length })"
|
||||
>
|
||||
+{{ hiddenTags.length }}
|
||||
</button>
|
||||
<template #content>
|
||||
<div
|
||||
class="flex max-w-56 flex-wrap gap-1.5 p-2"
|
||||
>
|
||||
<TagBadge
|
||||
v-for="tag in hiddenTags"
|
||||
:key="tag.id"
|
||||
:tag="tag"
|
||||
@click="emit('tag-click', tag.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
<span
|
||||
v-else-if="emptyLabel"
|
||||
class="text-muted"
|
||||
>
|
||||
{{ emptyLabel }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -43,7 +43,7 @@ function onBack() {
|
||||
<template>
|
||||
<div
|
||||
class="upload-card-flip"
|
||||
:class="{ 'is-flipped': flipped }"
|
||||
:class="[`upload-card-flip--${kind}`, { 'is-flipped': flipped }]"
|
||||
>
|
||||
<div class="upload-card-flip-inner">
|
||||
<div class="upload-card-face upload-card-face--front">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadMediaKind } from '~/composables/useUploadPreferences'
|
||||
import {
|
||||
normalizeUploadTagNames,
|
||||
type UploadMediaKind
|
||||
} from '~/composables/useUploadPreferences'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** 每个上传页各自一份偏好 */
|
||||
@@ -17,20 +20,36 @@ const {
|
||||
autoCopyMarkdown,
|
||||
copyFormat,
|
||||
clientWebpQuality,
|
||||
lastTagIds,
|
||||
uploadTagNames,
|
||||
transcodeToHls,
|
||||
loadPreferences
|
||||
} = useUploadPreferences(props.kind)
|
||||
const { items: tagItems, fetchTags } = useTags()
|
||||
const uploadTagDraft = ref('')
|
||||
|
||||
/** 视频床 / 文件床没有图片压缩相关选项 */
|
||||
const isImageKind = computed(() => props.kind === 'image')
|
||||
const isVideoKind = computed(() => props.kind === 'video')
|
||||
const gridClass = computed(() => {
|
||||
if (isImageKind.value) return 'sm:grid-cols-3 sm:gap-6'
|
||||
if (isVideoKind.value) return 'sm:grid-cols-2 sm:gap-6'
|
||||
return 'mx-auto w-full max-w-md'
|
||||
const processingGridClass = computed(() => {
|
||||
if (isImageKind.value || isVideoKind.value) return 'md:col-start-1 md:row-start-1'
|
||||
return ''
|
||||
})
|
||||
const qualityGridClass = computed(() => {
|
||||
if (isImageKind.value) return 'md:col-start-1 md:row-start-2'
|
||||
return ''
|
||||
})
|
||||
const convenienceGridClass = computed(() => {
|
||||
if (isImageKind.value) return 'md:col-start-2 md:row-start-1'
|
||||
if (isVideoKind.value) return 'md:col-start-1 md:row-start-2'
|
||||
return ''
|
||||
})
|
||||
const tagsGridClass = computed(() => {
|
||||
if (isImageKind.value) return 'md:col-start-2 md:row-start-2'
|
||||
if (isVideoKind.value) return 'md:col-start-2 md:row-start-1 md:row-span-2'
|
||||
return ''
|
||||
})
|
||||
const tagsLayoutClass = computed(() => [
|
||||
tagsGridClass.value
|
||||
])
|
||||
|
||||
const { t } = useI18n()
|
||||
const {
|
||||
@@ -74,11 +93,39 @@ function onClientQualityInput(event: Event) {
|
||||
clientWebpQuality.value = Number.isFinite(value) ? value : 80
|
||||
}
|
||||
|
||||
function commitUploadTags() {
|
||||
const names = normalizeUploadTagNames(uploadTagDraft.value)
|
||||
if (!names.length) return
|
||||
uploadTagNames.value = normalizeUploadTagNames([
|
||||
...uploadTagNames.value,
|
||||
...names
|
||||
])
|
||||
uploadTagDraft.value = ''
|
||||
}
|
||||
|
||||
function onUploadTagKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' || event.key === ',' || event.key === ',') {
|
||||
event.preventDefault()
|
||||
commitUploadTags()
|
||||
return
|
||||
}
|
||||
if (
|
||||
event.key === 'Backspace'
|
||||
&& !uploadTagDraft.value
|
||||
&& uploadTagNames.value.length
|
||||
) {
|
||||
uploadTagNames.value = uploadTagNames.value.slice(0, -1)
|
||||
}
|
||||
}
|
||||
|
||||
function removeUploadTag(name: string) {
|
||||
uploadTagNames.value = uploadTagNames.value.filter(item => item !== name)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPreferences()
|
||||
void loadUserSettings()
|
||||
void loadTranscodeSettings()
|
||||
void fetchTags()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -113,12 +160,12 @@ onMounted(() => {
|
||||
|
||||
<div class="flex flex-1 flex-col justify-start overflow-y-auto p-5 sm:p-6">
|
||||
<div
|
||||
class="grid gap-5"
|
||||
:class="gridClass"
|
||||
class="grid gap-5 md:grid-cols-2 md:gap-6"
|
||||
>
|
||||
<div
|
||||
v-if="isImageKind"
|
||||
class="space-y-4"
|
||||
class="space-y-4 rounded-xl border border-default/70 bg-muted/10 p-4"
|
||||
:class="processingGridClass"
|
||||
>
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ t('preferences.processing') }}
|
||||
@@ -179,7 +226,8 @@ onMounted(() => {
|
||||
|
||||
<div
|
||||
v-if="isVideoKind"
|
||||
class="space-y-4"
|
||||
class="space-y-4 rounded-xl border border-default/70 bg-muted/10 p-4"
|
||||
:class="processingGridClass"
|
||||
>
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ t('preferences.videoProcessing') }}
|
||||
@@ -217,7 +265,8 @@ onMounted(() => {
|
||||
|
||||
<div
|
||||
v-if="isImageKind"
|
||||
class="space-y-4"
|
||||
class="space-y-4 rounded-xl border border-default/70 bg-muted/10 p-4"
|
||||
:class="qualityGridClass"
|
||||
>
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ t('preferences.webpQuality') }}
|
||||
@@ -239,7 +288,10 @@ onMounted(() => {
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
class="space-y-4 rounded-xl border border-default/70 bg-muted/10 p-4"
|
||||
:class="convenienceGridClass"
|
||||
>
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ t('preferences.convenience') }}
|
||||
</h3>
|
||||
@@ -271,19 +323,57 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 space-y-3 border-t border-default pt-5">
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ t('preferences.uploadTags') }}
|
||||
</h3>
|
||||
<p class="text-xs leading-relaxed text-muted">
|
||||
{{ t('preferences.uploadTagsHint') }}
|
||||
</p>
|
||||
<TagAssignPanel
|
||||
v-model="lastTagIds"
|
||||
:tags="tagItems"
|
||||
/>
|
||||
<div
|
||||
class="space-y-3 rounded-xl border border-default/70 bg-muted/10 p-4"
|
||||
:class="tagsLayoutClass"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ t('preferences.uploadTags') }}
|
||||
</h3>
|
||||
<p class="text-xs leading-relaxed text-muted">
|
||||
{{ t('preferences.uploadTagsHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<UInput
|
||||
v-model="uploadTagDraft"
|
||||
size="md"
|
||||
icon="i-lucide-tags"
|
||||
maxlength="32"
|
||||
:placeholder="t('preferences.uploadTagsPlaceholder')"
|
||||
@keydown="onUploadTagKeydown"
|
||||
@blur="commitUploadTags"
|
||||
/>
|
||||
<div
|
||||
v-if="uploadTagNames.length"
|
||||
class="space-y-2 pt-1"
|
||||
>
|
||||
<p class="text-xs font-medium text-muted">
|
||||
{{ t('preferences.uploadTagsPending', { n: uploadTagNames.length }) }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2.5">
|
||||
<span
|
||||
v-for="name in uploadTagNames"
|
||||
:key="name"
|
||||
class="relative inline-flex max-w-full items-center rounded-full border border-primary/35 bg-primary/10 px-3 py-1 text-xs font-medium text-primary"
|
||||
>
|
||||
<span class="truncate">{{ name }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute -top-1.5 -right-1.5 inline-flex size-4 items-center justify-center rounded-full border border-primary/40 bg-elevated text-primary shadow-sm transition-colors hover:bg-primary hover:text-inverted"
|
||||
:aria-label="t('preferences.uploadTagsRemove', { name })"
|
||||
@click="removeUploadTag(name)"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-x"
|
||||
class="size-2.5"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,16 +109,7 @@ const uploadedLabel = computed(() => {
|
||||
<p class="truncate text-xs text-muted">
|
||||
{{ uploadedLabel }} · {{ formatFileSize(item.size) }}
|
||||
</p>
|
||||
<div
|
||||
v-if="item.tags?.length"
|
||||
class="flex flex-wrap gap-1"
|
||||
>
|
||||
<TagBadge
|
||||
v-for="tag in item.tags"
|
||||
:key="tag.id"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
<TagBadgeList :tags="item.tags" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-around border-t border-default px-2 py-2">
|
||||
|
||||
@@ -659,20 +659,11 @@ defineExpose({ refresh })
|
||||
{{ uploadedLabel(item) }}
|
||||
</td>
|
||||
<td class="hidden px-3 py-2 md:table-cell">
|
||||
<div
|
||||
v-if="item.tags?.length"
|
||||
class="flex flex-wrap gap-1"
|
||||
>
|
||||
<TagBadge
|
||||
v-for="tag in item.tags"
|
||||
:key="tag.id"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
class="text-muted"
|
||||
>{{ t('tags.unlabeled') }}</span>
|
||||
<TagBadgeList
|
||||
:tags="item.tags"
|
||||
compact
|
||||
:empty-label="t('tags.unlabeled')"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
|
||||
@@ -5,7 +5,9 @@ type VideoExtension = 'mp4' | 'm4v' | 'webm' | 'mov' | 'mkv' | 'avi' | 'flv' | '
|
||||
|
||||
const props = defineProps<{
|
||||
part: 'upload-preference' | 'image-processing' | 'login-rate'
|
||||
uploadMaxFileSizeMbSource?: PolicySource
|
||||
uploadImageMaxFileSizeMbSource?: PolicySource
|
||||
uploadVideoMaxFileSizeMbSource?: PolicySource
|
||||
uploadFileMaxFileSizeMbSource?: PolicySource
|
||||
allowedMimeTypesSource?: PolicySource
|
||||
allowedVideoExtensionsSource?: PolicySource
|
||||
forbiddenFileExtensionsSource?: PolicySource
|
||||
@@ -20,7 +22,9 @@ const props = defineProps<{
|
||||
forbiddenFileExtensionOptions?: ReadonlyArray<{ extension: string, label: string }>
|
||||
}>()
|
||||
|
||||
const uploadMaxFileSizeMbDraft = defineModel<number>('uploadMaxFileSizeMb')
|
||||
const uploadImageMaxFileSizeMbDraft = defineModel<number>('uploadImageMaxFileSizeMb')
|
||||
const uploadVideoMaxFileSizeMbDraft = defineModel<number>('uploadVideoMaxFileSizeMb')
|
||||
const uploadFileMaxFileSizeMbDraft = defineModel<number>('uploadFileMaxFileSizeMb')
|
||||
const allowedMimeTypesDraft = defineModel<MimeType[]>('allowedMimeTypes')
|
||||
const allowedVideoExtensionsDraft = defineModel<VideoExtension[]>('allowedVideoExtensions')
|
||||
const forbiddenFileExtensionsDraft = defineModel<string[]>('forbiddenFileExtensions')
|
||||
@@ -125,67 +129,109 @@ function addCustomForbiddenExtension() {
|
||||
customForbiddenExtension.value = ''
|
||||
}
|
||||
|
||||
function clampUploadMaxFileSizeMb(value: unknown): number {
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num)) return uploadMaxFileSizeMbDraft.value ?? 10
|
||||
return Math.min(100, Math.max(1, Math.round(num)))
|
||||
type UploadSizeKind = 'image' | 'video' | 'file'
|
||||
|
||||
const uploadSizeFields = computed(() => [
|
||||
{
|
||||
kind: 'image' as const,
|
||||
title: t('settings.uploadImageMaxFileSize'),
|
||||
hint: t('settings.uploadImageMaxFileSizeHint'),
|
||||
source: props.uploadImageMaxFileSizeMbSource,
|
||||
value: uploadImageMaxFileSizeMbDraft.value ?? 10,
|
||||
max: 100
|
||||
},
|
||||
{
|
||||
kind: 'video' as const,
|
||||
title: t('settings.uploadVideoMaxFileSize'),
|
||||
hint: t('settings.uploadVideoMaxFileSizeHint'),
|
||||
source: props.uploadVideoMaxFileSizeMbSource,
|
||||
value: uploadVideoMaxFileSizeMbDraft.value ?? 500,
|
||||
max: 2048
|
||||
},
|
||||
{
|
||||
kind: 'file' as const,
|
||||
title: t('settings.uploadFileMaxFileSize'),
|
||||
hint: t('settings.uploadFileMaxFileSizeHint'),
|
||||
source: props.uploadFileMaxFileSizeMbSource,
|
||||
value: uploadFileMaxFileSizeMbDraft.value ?? 100,
|
||||
max: 1024
|
||||
}
|
||||
])
|
||||
|
||||
function uploadSizeModel(kind: UploadSizeKind) {
|
||||
if (kind === 'video') return uploadVideoMaxFileSizeMbDraft
|
||||
if (kind === 'file') return uploadFileMaxFileSizeMbDraft
|
||||
return uploadImageMaxFileSizeMbDraft
|
||||
}
|
||||
|
||||
function onUploadMaxFileSizeInput(event: Event) {
|
||||
uploadMaxFileSizeMbDraft.value = clampUploadMaxFileSizeMb(
|
||||
function clampUploadMaxFileSizeMb(kind: UploadSizeKind, value: unknown): number {
|
||||
const num = Number(value)
|
||||
const model = uploadSizeModel(kind)
|
||||
const max = kind === 'video' ? 2048 : kind === 'file' ? 1024 : 100
|
||||
if (!Number.isFinite(num)) return model.value ?? (kind === 'video' ? 500 : kind === 'file' ? 100 : 10)
|
||||
return Math.min(max, Math.max(0, Math.round(num)))
|
||||
}
|
||||
|
||||
function onUploadMaxFileSizeInput(kind: UploadSizeKind, event: Event) {
|
||||
uploadSizeModel(kind).value = clampUploadMaxFileSizeMb(kind,
|
||||
(event.target as HTMLInputElement).value
|
||||
)
|
||||
}
|
||||
|
||||
function onUploadMaxFileSizeNumberInput(value: string | number) {
|
||||
uploadMaxFileSizeMbDraft.value = clampUploadMaxFileSizeMb(value)
|
||||
function onUploadMaxFileSizeNumberInput(kind: UploadSizeKind, value: string | number) {
|
||||
uploadSizeModel(kind).value = clampUploadMaxFileSizeMb(kind, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="part === 'upload-preference'">
|
||||
<div class="py-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<p class="text-sm font-medium text-highlighted">
|
||||
{{ t('settings.uploadMaxFileSize') }}
|
||||
</p>
|
||||
<UBadge
|
||||
v-if="uploadMaxFileSizeMbSource"
|
||||
:color="sourceBadge(uploadMaxFileSizeMbSource).color"
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
>
|
||||
{{ sourceBadge(uploadMaxFileSizeMbSource).label }}
|
||||
</UBadge>
|
||||
</div>
|
||||
<p class="mt-1 text-xs leading-relaxed text-muted">
|
||||
{{ t('settings.uploadMaxFileSizeHint') }}
|
||||
</p>
|
||||
<div class="space-y-5 py-4">
|
||||
<div
|
||||
class="mt-3 flex items-center gap-3"
|
||||
:class="uploadMaxFileSizeMbSource === 'env' ? 'pointer-events-none opacity-60' : ''"
|
||||
v-for="field in uploadSizeFields"
|
||||
:key="field.kind"
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
:value="uploadMaxFileSizeMbDraft"
|
||||
class="h-1.5 min-w-0 flex-1 cursor-pointer appearance-none rounded-full bg-muted accent-primary"
|
||||
:disabled="uploadMaxFileSizeMbSource === 'env'"
|
||||
@input="onUploadMaxFileSizeInput"
|
||||
>
|
||||
<div class="flex shrink-0 items-center gap-1.5">
|
||||
<UInput
|
||||
:model-value="uploadMaxFileSizeMbDraft"
|
||||
type="number"
|
||||
:min="1"
|
||||
:max="100"
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<p class="text-sm font-medium text-highlighted">
|
||||
{{ field.title }}
|
||||
</p>
|
||||
<UBadge
|
||||
v-if="field.source"
|
||||
:color="sourceBadge(field.source).color"
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
class="w-14 tabular-nums"
|
||||
:disabled="uploadMaxFileSizeMbSource === 'env'"
|
||||
@update:model-value="onUploadMaxFileSizeNumberInput"
|
||||
/>
|
||||
<span class="text-xs text-muted">MB</span>
|
||||
>
|
||||
{{ sourceBadge(field.source).label }}
|
||||
</UBadge>
|
||||
</div>
|
||||
<p class="mt-1 text-xs leading-relaxed text-muted">
|
||||
{{ field.hint }}
|
||||
</p>
|
||||
<div
|
||||
class="mt-3 flex items-center gap-3"
|
||||
:class="field.source === 'env' ? 'pointer-events-none opacity-60' : ''"
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
:max="field.max"
|
||||
:value="field.value"
|
||||
class="h-1.5 min-w-0 flex-1 cursor-pointer appearance-none rounded-full bg-muted accent-primary"
|
||||
:disabled="field.source === 'env'"
|
||||
@input="onUploadMaxFileSizeInput(field.kind, $event)"
|
||||
>
|
||||
<div class="flex shrink-0 items-center gap-1.5">
|
||||
<UInput
|
||||
:model-value="field.value"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="field.max"
|
||||
size="xs"
|
||||
class="w-16 tabular-nums"
|
||||
:disabled="field.source === 'env'"
|
||||
@update:model-value="onUploadMaxFileSizeNumberInput(field.kind, $event)"
|
||||
/>
|
||||
<span class="text-xs text-muted">MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,21 +4,22 @@ import type {
|
||||
} from '~/types/image'
|
||||
import { isUnauthorizedError } from './useAuth'
|
||||
import { prepareFilesForUpload } from './useClipboardImage'
|
||||
import { normalizeUploadTagNames } from './useUploadPreferences'
|
||||
|
||||
const MAX_FILES = 10
|
||||
|
||||
export function useImageUpload() {
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
const { compressEnabled, clientWebpQuality, lastTagIds } = useUploadPreferences()
|
||||
const { items: tagItems } = useTags()
|
||||
const { compressEnabled, clientWebpQuality, uploadTagNames } = useUploadPreferences()
|
||||
const { load: loadUploadPolicy } = useUploadPolicy()
|
||||
const uploading = ref(false)
|
||||
const progressItems = ref<UploadProgressItem[]>([])
|
||||
const lastUploadResult = ref<UploadResponse | null>(null)
|
||||
|
||||
async function uploadFiles(
|
||||
files: File[],
|
||||
options?: { notify?: boolean, tagIds?: number[] }
|
||||
options?: { notify?: boolean }
|
||||
) {
|
||||
if (uploading.value) return
|
||||
|
||||
@@ -43,10 +44,27 @@ export function useImageUpload() {
|
||||
}))
|
||||
|
||||
try {
|
||||
const prepared = await prepareFilesForUpload(files, {
|
||||
const preparedFiles = await prepareFilesForUpload(files, {
|
||||
enabled: compressEnabled.value,
|
||||
quality: clientWebpQuality.value / 100
|
||||
})
|
||||
const currentPolicy = await loadUploadPolicy().catch(() => null)
|
||||
const maxFileSizeMb = currentPolicy?.imageMaxFileSizeMb ?? 10
|
||||
const prepared = preparedFiles.filter((file) => {
|
||||
if (file.size <= maxFileSizeMb * 1024 * 1024) return true
|
||||
toast.add({
|
||||
title: t('files.fileTooLarge', {
|
||||
name: file.name,
|
||||
limit: maxFileSizeMb
|
||||
}),
|
||||
color: 'error'
|
||||
})
|
||||
return false
|
||||
})
|
||||
if (!prepared.length) {
|
||||
progressItems.value = []
|
||||
return
|
||||
}
|
||||
|
||||
progressItems.value = prepared.map((file, index) => ({
|
||||
name: file.name,
|
||||
@@ -59,15 +77,9 @@ export function useImageUpload() {
|
||||
formData.append('files', file, file.name)
|
||||
}
|
||||
|
||||
const tagIds = options?.tagIds ?? lastTagIds.value
|
||||
if (tagIds.length) {
|
||||
const names = tagIds
|
||||
.map(id => tagItems.value.find(tag => tag.id === id)?.name)
|
||||
.filter((name): name is string => Boolean(name))
|
||||
if (names.length) {
|
||||
formData.append('tagNames', JSON.stringify(names))
|
||||
}
|
||||
lastTagIds.value = tagIds
|
||||
const tagNames = normalizeUploadTagNames(uploadTagNames.value).join(',')
|
||||
if (tagNames) {
|
||||
formData.append('tagNames', tagNames)
|
||||
}
|
||||
|
||||
const response = await $fetch<UploadResponse>('/api/images/upload', {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { UploadProgressItem } from '~/types/image'
|
||||
import type { FileItem, FileListResponse, FileUploadResponse } from '~/types/file'
|
||||
import type { CopyFormat, UploadMediaKind } from './useUploadPreferences'
|
||||
import { isUnauthorizedError } from './useAuth'
|
||||
import { normalizeUploadTagNames } from './useUploadPreferences'
|
||||
|
||||
const MAX_FILES = 10
|
||||
|
||||
@@ -54,10 +55,9 @@ export function useMediaUpload(kind: UploadMediaKind) {
|
||||
autoCopyMarkdown,
|
||||
copyFormat,
|
||||
transcodeToHls,
|
||||
lastTagIds,
|
||||
uploadTagNames,
|
||||
loadPreferences
|
||||
} = useUploadPreferences(kind)
|
||||
const { items: tagItems, fetchTags } = useTags()
|
||||
|
||||
const uploading = ref(false)
|
||||
const progressItems = ref<UploadProgressItem[]>([])
|
||||
@@ -209,6 +209,19 @@ export function useMediaUpload(kind: UploadMediaKind) {
|
||||
})
|
||||
return false
|
||||
}
|
||||
const maxFileSizeMb = kind === 'video'
|
||||
? (currentPolicy?.videoMaxFileSizeMb ?? 500)
|
||||
: (currentPolicy?.fileMaxFileSizeMb ?? 100)
|
||||
if (file.size > maxFileSizeMb * 1024 * 1024) {
|
||||
toast.add({
|
||||
title: t('files.fileTooLarge', {
|
||||
name: file.name,
|
||||
limit: maxFileSizeMb
|
||||
}),
|
||||
color: 'error'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (!acceptedFiles.length) return
|
||||
@@ -234,14 +247,9 @@ export function useMediaUpload(kind: UploadMediaKind) {
|
||||
if (kind === 'video' && transcodeToHls.value) {
|
||||
formData.append('transcode', '1')
|
||||
}
|
||||
if (lastTagIds.value.length) {
|
||||
await fetchTags()
|
||||
const names = lastTagIds.value
|
||||
.map(id => tagItems.value.find(tag => tag.id === id)?.name)
|
||||
.filter((name): name is string => Boolean(name))
|
||||
if (names.length) {
|
||||
formData.append('tagNames', JSON.stringify(names))
|
||||
}
|
||||
const tagNames = normalizeUploadTagNames(uploadTagNames.value).join(',')
|
||||
if (tagNames) {
|
||||
formData.append('tagNames', tagNames)
|
||||
}
|
||||
|
||||
progressItems.value = progressItems.value.map(item => ({
|
||||
|
||||
@@ -2,6 +2,9 @@ interface UploadPolicy {
|
||||
allowedMimeTypes: string[]
|
||||
allowedVideoExtensions: string[]
|
||||
forbiddenFileExtensions: string[]
|
||||
imageMaxFileSizeMb: number
|
||||
videoMaxFileSizeMb: number
|
||||
fileMaxFileSizeMb: number
|
||||
}
|
||||
|
||||
const policy = ref<UploadPolicy | null>(null)
|
||||
@@ -21,7 +24,10 @@ export function useUploadPolicy() {
|
||||
policy.value = {
|
||||
allowedMimeTypes: [...data.allowedMimeTypes],
|
||||
allowedVideoExtensions: [...data.allowedVideoExtensions],
|
||||
forbiddenFileExtensions: [...data.forbiddenFileExtensions]
|
||||
forbiddenFileExtensions: [...data.forbiddenFileExtensions],
|
||||
imageMaxFileSizeMb: data.imageMaxFileSizeMb,
|
||||
videoMaxFileSizeMb: data.videoMaxFileSizeMb,
|
||||
fileMaxFileSizeMb: data.fileMaxFileSizeMb
|
||||
}
|
||||
return policy.value
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface UploadPreferences {
|
||||
autoCopyMarkdown: boolean
|
||||
copyFormat: CopyFormat
|
||||
clientWebpQuality: number
|
||||
lastTagIds: number[]
|
||||
uploadTagNames: string[]
|
||||
/** 视频床专属:上传后转码为 HLS(m3u8 + 分片) */
|
||||
transcodeToHls: boolean
|
||||
}
|
||||
@@ -23,10 +23,21 @@ const DEFAULTS: UploadPreferences = {
|
||||
autoCopyMarkdown: false,
|
||||
copyFormat: 'markdown',
|
||||
clientWebpQuality: 80,
|
||||
lastTagIds: [],
|
||||
uploadTagNames: [],
|
||||
transcodeToHls: false
|
||||
}
|
||||
|
||||
export function normalizeUploadTagNames(value: string | readonly string[]): string[] {
|
||||
const parts = typeof value === 'string'
|
||||
? value.split(/[,,\n]/)
|
||||
: value.flatMap(item => String(item).split(/[,,\n]/))
|
||||
return [...new Set(
|
||||
parts
|
||||
.map(name => name.trim())
|
||||
.filter(name => Boolean(name) && name.length <= 32)
|
||||
)]
|
||||
}
|
||||
|
||||
function clampQuality(value: unknown): number {
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num)) return DEFAULTS.clientWebpQuality
|
||||
@@ -49,9 +60,8 @@ function readStored(kind: UploadMediaKind): UploadPreferences {
|
||||
autoCopyMarkdown: parsed.autoCopyMarkdown === true,
|
||||
copyFormat: isCopyFormat(parsed.copyFormat) ? parsed.copyFormat : DEFAULTS.copyFormat,
|
||||
clientWebpQuality: clampQuality(parsed.clientWebpQuality),
|
||||
lastTagIds: Array.isArray(parsed.lastTagIds)
|
||||
? parsed.lastTagIds.filter(id => Number.isInteger(id) && id > 0)
|
||||
: DEFAULTS.lastTagIds,
|
||||
/** 标签输入只在当前页面会话内生效,刷新后默认为空。 */
|
||||
uploadTagNames: [...DEFAULTS.uploadTagNames],
|
||||
transcodeToHls: parsed.transcodeToHls === true
|
||||
}
|
||||
} catch {
|
||||
@@ -69,17 +79,23 @@ export function useUploadPreferences(kind: UploadMediaKind = 'image') {
|
||||
const autoCopyMarkdown = useState(`upload-pref-auto-md-${kind}`, () => DEFAULTS.autoCopyMarkdown)
|
||||
const copyFormat = useState<CopyFormat>(`upload-pref-copy-format-${kind}`, () => DEFAULTS.copyFormat)
|
||||
const clientWebpQuality = useState(`upload-pref-webp-quality-${kind}`, () => DEFAULTS.clientWebpQuality)
|
||||
const lastTagIds = useState<number[]>(`upload-pref-last-tag-ids-${kind}`, () => DEFAULTS.lastTagIds)
|
||||
const uploadTagNames = useState<string[]>(
|
||||
`upload-pref-tag-names-${kind}`,
|
||||
() => [...DEFAULTS.uploadTagNames]
|
||||
)
|
||||
const transcodeToHls = useState(`upload-pref-transcode-${kind}`, () => DEFAULTS.transcodeToHls)
|
||||
const loaded = useState(`upload-pref-loaded-${kind}`, () => false)
|
||||
|
||||
function loadPreferences() {
|
||||
const firstLoad = !loaded.value
|
||||
const stored = readStored(kind)
|
||||
compressEnabled.value = stored.compressEnabled
|
||||
autoCopyMarkdown.value = stored.autoCopyMarkdown
|
||||
copyFormat.value = stored.copyFormat
|
||||
clientWebpQuality.value = stored.clientWebpQuality
|
||||
lastTagIds.value = stored.lastTagIds
|
||||
if (firstLoad) {
|
||||
uploadTagNames.value = [...stored.uploadTagNames]
|
||||
}
|
||||
transcodeToHls.value = stored.transcodeToHls
|
||||
loaded.value = true
|
||||
}
|
||||
@@ -94,7 +110,7 @@ export function useUploadPreferences(kind: UploadMediaKind = 'image') {
|
||||
autoCopyMarkdown: autoCopyMarkdown.value,
|
||||
copyFormat: copyFormat.value,
|
||||
clientWebpQuality: clampQuality(clientWebpQuality.value),
|
||||
lastTagIds: lastTagIds.value,
|
||||
uploadTagNames: [...DEFAULTS.uploadTagNames],
|
||||
transcodeToHls: transcodeToHls.value
|
||||
})
|
||||
}
|
||||
@@ -104,12 +120,13 @@ export function useUploadPreferences(kind: UploadMediaKind = 'image') {
|
||||
autoCopyMarkdown.value = DEFAULTS.autoCopyMarkdown
|
||||
copyFormat.value = DEFAULTS.copyFormat
|
||||
clientWebpQuality.value = DEFAULTS.clientWebpQuality
|
||||
uploadTagNames.value = [...DEFAULTS.uploadTagNames]
|
||||
transcodeToHls.value = DEFAULTS.transcodeToHls
|
||||
savePreferences()
|
||||
}
|
||||
|
||||
watch(
|
||||
[compressEnabled, autoCopyMarkdown, copyFormat, clientWebpQuality, lastTagIds, transcodeToHls],
|
||||
[compressEnabled, autoCopyMarkdown, copyFormat, clientWebpQuality, transcodeToHls],
|
||||
() => {
|
||||
if (loaded.value) {
|
||||
savePreferences()
|
||||
@@ -123,7 +140,7 @@ export function useUploadPreferences(kind: UploadMediaKind = 'image') {
|
||||
autoCopyMarkdown,
|
||||
copyFormat,
|
||||
clientWebpQuality,
|
||||
lastTagIds,
|
||||
uploadTagNames,
|
||||
transcodeToHls,
|
||||
loaded,
|
||||
loadPreferences,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ImageItem, ImageTag } from '~/types/image'
|
||||
import { buildImageBbcode } from '~/utils/image-display'
|
||||
|
||||
const { uploading, progressItems, uploadFiles } = useImageUpload()
|
||||
const { policy: uploadPolicy, load: loadUploadPolicy } = useUploadPolicy()
|
||||
|
||||
const {
|
||||
autoCopyMarkdown,
|
||||
@@ -16,6 +17,9 @@ const { recordRecent } = useRecentTags()
|
||||
const { isChecking, isAuthenticated, checkSession, handleAuthError, fetchStatus } = useAuth()
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
const imageFormats = computed(() =>
|
||||
t('upload.formats', { size: uploadPolicy.value?.imageMaxFileSizeMb ?? 10 })
|
||||
)
|
||||
const sessionItems = ref<ImageItem[]>([])
|
||||
const deletingKeys = ref<Set<string>>(new Set())
|
||||
const emptySelection = ref(new Set<string>())
|
||||
@@ -39,6 +43,7 @@ const showProgress = computed(
|
||||
async function loadPage() {
|
||||
try {
|
||||
loadPreferences()
|
||||
await loadUploadPolicy().catch(() => {})
|
||||
} catch (error: unknown) {
|
||||
handleAuthError(error)
|
||||
}
|
||||
@@ -227,6 +232,7 @@ async function confirmBatchTags(tagIds: number[]) {
|
||||
<AppShell v-else>
|
||||
<UploadFlipCard
|
||||
:disabled="uploading"
|
||||
:formats="imageFormats"
|
||||
@upload="handleUpload"
|
||||
/>
|
||||
|
||||
|
||||
@@ -55,8 +55,12 @@ interface SettingsResponse {
|
||||
capApiEndpoint: string
|
||||
capSecret: string
|
||||
appVersion: string
|
||||
uploadMaxFileSizeMb: number
|
||||
uploadMaxFileSizeMbSource: PolicySource
|
||||
uploadImageMaxFileSizeMb: number
|
||||
uploadImageMaxFileSizeMbSource: PolicySource
|
||||
uploadVideoMaxFileSizeMb: number
|
||||
uploadVideoMaxFileSizeMbSource: PolicySource
|
||||
uploadFileMaxFileSizeMb: number
|
||||
uploadFileMaxFileSizeMbSource: PolicySource
|
||||
allowedMimeTypes: MimeType[]
|
||||
allowedMimeTypesSource: PolicySource
|
||||
allowedVideoExtensions: VideoExtension[]
|
||||
@@ -185,7 +189,9 @@ const capApiEndpointDraft = ref('')
|
||||
const capSecretDraft = ref('')
|
||||
const domainSeparationSaveConfirmOpen = ref(false)
|
||||
|
||||
const uploadMaxFileSizeMbDraft = ref(10)
|
||||
const uploadImageMaxFileSizeMbDraft = ref(10)
|
||||
const uploadVideoMaxFileSizeMbDraft = ref(500)
|
||||
const uploadFileMaxFileSizeMbDraft = ref(100)
|
||||
const allowedMimeTypesDraft = ref<MimeType[]>([])
|
||||
const allowedVideoExtensionsDraft = ref<VideoExtension[]>([])
|
||||
const forbiddenFileExtensionsDraft = ref<string[]>([])
|
||||
@@ -242,7 +248,9 @@ const hasServerChanges = computed(() => {
|
||||
|| turnstileSecretKeyDraft.value !== settings.value.turnstileSecretKey
|
||||
|| capApiEndpointDraft.value !== settings.value.capApiEndpoint
|
||||
|| capSecretDraft.value !== settings.value.capSecret
|
||||
|| uploadMaxFileSizeMbDraft.value !== settings.value.uploadMaxFileSizeMb
|
||||
|| uploadImageMaxFileSizeMbDraft.value !== settings.value.uploadImageMaxFileSizeMb
|
||||
|| uploadVideoMaxFileSizeMbDraft.value !== settings.value.uploadVideoMaxFileSizeMb
|
||||
|| uploadFileMaxFileSizeMbDraft.value !== settings.value.uploadFileMaxFileSizeMb
|
||||
|| !mimeListsEqual(allowedMimeTypesDraft.value, settings.value.allowedMimeTypes)
|
||||
|| !mimeListsEqual(allowedVideoExtensionsDraft.value, settings.value.allowedVideoExtensions)
|
||||
|| !mimeListsEqual(forbiddenFileExtensionsDraft.value, settings.value.forbiddenFileExtensions)
|
||||
@@ -320,7 +328,9 @@ function applySettings(data: SettingsResponse) {
|
||||
turnstileSecretKeyDraft.value = data.turnstileSecretKey
|
||||
capApiEndpointDraft.value = data.capApiEndpoint
|
||||
capSecretDraft.value = data.capSecret
|
||||
uploadMaxFileSizeMbDraft.value = data.uploadMaxFileSizeMb
|
||||
uploadImageMaxFileSizeMbDraft.value = data.uploadImageMaxFileSizeMb
|
||||
uploadVideoMaxFileSizeMbDraft.value = data.uploadVideoMaxFileSizeMb
|
||||
uploadFileMaxFileSizeMbDraft.value = data.uploadFileMaxFileSizeMb
|
||||
allowedMimeTypesDraft.value = [...data.allowedMimeTypes]
|
||||
allowedVideoExtensionsDraft.value = [...data.allowedVideoExtensions]
|
||||
forbiddenFileExtensionsDraft.value = [...data.forbiddenFileExtensions]
|
||||
@@ -423,7 +433,9 @@ async function performSaveServerSettings() {
|
||||
turnstileSecretKey: turnstileSecretKeyDraft.value,
|
||||
capApiEndpoint: capApiEndpointDraft.value,
|
||||
capSecret: capSecretDraft.value,
|
||||
uploadMaxFileSizeMb: uploadMaxFileSizeMbDraft.value,
|
||||
uploadImageMaxFileSizeMb: uploadImageMaxFileSizeMbDraft.value,
|
||||
uploadVideoMaxFileSizeMb: uploadVideoMaxFileSizeMbDraft.value,
|
||||
uploadFileMaxFileSizeMb: uploadFileMaxFileSizeMbDraft.value,
|
||||
allowedMimeTypes: allowedMimeTypesDraft.value,
|
||||
allowedVideoExtensions: allowedVideoExtensionsDraft.value,
|
||||
forbiddenFileExtensions: forbiddenFileExtensionsDraft.value,
|
||||
@@ -626,7 +638,9 @@ watch(isAdmin, () => {
|
||||
>
|
||||
<SettingsGroup>
|
||||
<SettingsUploadPolicyFields
|
||||
v-model:upload-max-file-size-mb="uploadMaxFileSizeMbDraft"
|
||||
v-model:upload-image-max-file-size-mb="uploadImageMaxFileSizeMbDraft"
|
||||
v-model:upload-video-max-file-size-mb="uploadVideoMaxFileSizeMbDraft"
|
||||
v-model:upload-file-max-file-size-mb="uploadFileMaxFileSizeMbDraft"
|
||||
v-model:allowed-mime-types="allowedMimeTypesDraft"
|
||||
v-model:allowed-video-extensions="allowedVideoExtensionsDraft"
|
||||
v-model:forbidden-file-extensions="forbiddenFileExtensionsDraft"
|
||||
@@ -634,7 +648,9 @@ watch(isAdmin, () => {
|
||||
v-model:upload-rate-token-max="uploadRateTokenMaxDraft"
|
||||
v-model:upload-rate-window-minutes="uploadRateWindowMinutesDraft"
|
||||
part="upload-preference"
|
||||
:upload-max-file-size-mb-source="settings.uploadMaxFileSizeMbSource"
|
||||
:upload-image-max-file-size-mb-source="settings.uploadImageMaxFileSizeMbSource"
|
||||
:upload-video-max-file-size-mb-source="settings.uploadVideoMaxFileSizeMbSource"
|
||||
:upload-file-max-file-size-mb-source="settings.uploadFileMaxFileSizeMbSource"
|
||||
:allowed-mime-types-source="settings.allowedMimeTypesSource"
|
||||
:allowed-video-extensions-source="settings.allowedVideoExtensionsSource"
|
||||
:forbidden-file-extensions-source="settings.forbiddenFileExtensionsSource"
|
||||
|
||||
Reference in new issue
Block a user