服务器配置

This commit is contained in:
tianmo committed 2026-10-17 16:09:31 +08:00
1 parent a1b4a63552
commit fa7c8244b0
33 files changed
+1882 -363

No files matched your search

+41 -12
View File
@@ -8,6 +8,10 @@ const props = defineProps<{
const { t } = useI18n()
const toast = useToast()
const {
policy: uploadPolicy,
load: loadUploadPolicy
} = useUploadPolicy()
const {
uploading,
progressItems,
@@ -19,20 +23,44 @@ const {
} = useMediaUpload(props.kind)
onMounted(() => {
void loadActiveTranscodes()
void Promise.all([
loadActiveTranscodes(),
loadUploadPolicy().catch(() => {})
])
})
const text = computed(() => props.kind === 'video'
? {
uploadTitle: t('library.video.uploadTitle'),
uploadHint: t('library.video.uploadHint'),
uploadFormats: t('library.video.uploadFormats')
}
: {
uploadTitle: t('library.file.uploadTitle'),
uploadHint: t('library.file.uploadHint'),
uploadFormats: t('library.file.uploadFormats')
})
const text = computed(() => {
const allowedVideos = uploadPolicy.value?.allowedVideoExtensions ?? []
const forbiddenFiles = uploadPolicy.value?.forbiddenFileExtensions ?? []
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(' / ')
})
: t('library.video.uploadFormats')
}
: {
uploadTitle: t('library.file.uploadTitle'),
uploadHint: t('library.file.uploadHint'),
uploadFormats: forbiddenFiles.length
? t('library.file.uploadFormatsBlocked', {
formats: forbiddenFiles.map(item => item.toUpperCase()).join(' / ')
})
: t('library.file.uploadFormatsUnrestricted')
}
})
const accept = computed(() => {
if (props.kind !== 'video') return undefined
const extensions = uploadPolicy.value?.allowedVideoExtensions ?? []
if (!extensions.length) return undefined
return extensions
.map(extension => `.${extension}`)
.join(',')
})
const showProgress = computed(() =>
uploading.value
@@ -66,6 +94,7 @@ async function removeItem(item: FileItem) {
:title="text.uploadTitle"
:hint="text.uploadHint"
:formats="text.uploadFormats"
:accept="accept"
:disabled="uploading"
@upload="uploadFiles"
/>
@@ -0,0 +1,106 @@
<script setup lang="ts">
const { t } = useI18n()
const { isAdmin } = useAuth()
const {
concurrency,
threads,
maxConcurrency,
maxThreads,
preset,
bitratePercent,
minBitratePercent,
maxBitratePercent,
load
} = useTranscodeSettings()
const presetItems = computed(() => [
{ value: 'copy', label: t('preferences.presetCopy') },
{ value: 'veryfast', label: t('preferences.presetSpeed') },
{ value: 'medium', label: t('preferences.presetBalanced') },
{ value: 'slow', label: t('preferences.presetQuality') },
{ value: 'compress', label: t('preferences.presetCompress') }
])
function onBitrateInput(event: Event) {
const value = Number((event.target as HTMLInputElement).value)
bitratePercent.value = Number.isFinite(value) ? value : 80
}
function selectPreset(value: string) {
preset.value = value
}
onMounted(() => {
void load()
})
</script>
<template>
<div class="space-y-3">
<div class="flex items-center gap-2 text-sm">
<span class="shrink-0">{{ t('preferences.transcodeConcurrency') }}</span>
<UInput
v-model.number="concurrency"
type="number"
min="1"
:max="maxConcurrency"
size="xs"
class="w-16 tabular-nums"
:disabled="!isAdmin"
/>
<span class="text-muted">{{ t('preferences.unitCount') }}</span>
<span class="text-xs text-muted">{{ t('preferences.concurrentHint') }}</span>
</div>
<div class="flex items-center gap-2 text-sm">
<span class="shrink-0">{{ t('preferences.transcodeThreads') }}</span>
<UInput
v-model.number="threads"
type="number"
min="1"
:max="maxThreads"
size="xs"
class="w-16 tabular-nums"
:disabled="!isAdmin"
/>
<span class="text-muted">{{ t('preferences.unitCount') }}</span>
<span class="text-xs text-muted">{{ t('preferences.threadsHint') }}</span>
</div>
<div class="flex flex-wrap items-center gap-2 text-sm">
<span class="shrink-0">{{ t('preferences.transcodeQuality') }}</span>
<div class="flex gap-1 rounded-lg border border-default p-0.5">
<UButton
v-for="item in presetItems"
:key="item.value"
size="xs"
class="flex-1 justify-center"
:variant="preset === item.value ? 'solid' : 'ghost'"
:color="preset === item.value ? 'primary' : 'neutral'"
:label="item.label"
:disabled="!isAdmin"
@click="selectPreset(item.value)"
/>
</div>
</div>
<div
class="flex items-center gap-2 text-sm"
:class="preset === 'compress' ? '' : 'opacity-40'"
>
<span class="shrink-0">{{ t('preferences.compressBitrate') }}</span>
<input
type="range"
:min="minBitratePercent"
:max="maxBitratePercent"
:value="bitratePercent"
:disabled="!isAdmin || preset !== 'compress'"
class="h-2 min-w-24 flex-1 cursor-pointer appearance-none rounded-full bg-default accent-primary disabled:opacity-40"
@input="onBitrateInput"
>
<span class="w-12 shrink-0 text-right tabular-nums text-muted">
{{ bitratePercent }}%
</span>
</div>
</div>
</template>
+1 -96
View File
@@ -39,32 +39,6 @@ const {
load: loadUserSettings
} = useUserAutoDeleteSettings()
const { isAdmin } = useAuth()
const {
concurrency: transcodeConcurrency,
threads: transcodeThreads,
maxConcurrency: maxTranscodeConcurrency,
maxThreads: maxTranscodeThreads,
preset: transcodePreset,
bitratePercent: transcodeBitratePercent,
minBitratePercent,
maxBitratePercent,
load: loadTranscodeSettings
} = useTranscodeSettings()
const presetItems = computed(() => [
{ value: 'copy', label: t('preferences.presetCopy') },
{ value: 'veryfast', label: t('preferences.presetSpeed') },
{ value: 'medium', label: t('preferences.presetBalanced') },
{ value: 'slow', label: t('preferences.presetQuality') },
{ value: 'compress', label: t('preferences.presetCompress') }
])
function onBitrateInput(event: Event) {
const value = Number((event.target as HTMLInputElement).value)
transcodeBitratePercent.value = Number.isFinite(value) ? value : 80
}
const copyFormatItems = computed(() => [
{ label: t('copy.url'), value: 'url' as const },
{ label: t('copy.markdown'), value: 'markdown' as const },
@@ -87,14 +61,9 @@ function onClientQualityInput(event: Event) {
clientWebpQuality.value = Number.isFinite(value) ? value : 80
}
function selectPreset(value: string) {
transcodePreset.value = value
}
onMounted(() => {
loadPreferences()
void loadUserSettings()
void loadTranscodeSettings()
})
</script>
@@ -218,71 +187,7 @@ onMounted(() => {
v-if="transcodeToHls"
class="ml-7 space-y-3 border-t border-default pt-4"
>
<div class="flex items-center gap-2 text-sm">
<span class="shrink-0">{{ t('preferences.transcodeConcurrency') }}</span>
<UInput
v-model.number="transcodeConcurrency"
type="number"
min="1"
:max="maxTranscodeConcurrency"
size="xs"
class="w-16 tabular-nums"
:disabled="!isAdmin"
/>
<span class="text-muted">{{ t('preferences.unitCount') }}</span>
<span class="text-xs text-muted">{{ t('preferences.concurrentHint') }}</span>
</div>
<div class="flex items-center gap-2 text-sm">
<span class="shrink-0">{{ t('preferences.transcodeThreads') }}</span>
<UInput
v-model.number="transcodeThreads"
type="number"
min="1"
:max="maxTranscodeThreads"
size="xs"
class="w-16 tabular-nums"
:disabled="!isAdmin"
/>
<span class="text-muted">{{ t('preferences.unitCount') }}</span>
<span class="text-xs text-muted">{{ t('preferences.threadsHint') }}</span>
</div>
<div class="flex items-center gap-2 text-sm">
<span class="shrink-0">{{ t('preferences.transcodeQuality') }}</span>
<div class="flex gap-1 rounded-lg border border-default p-0.5">
<UButton
v-for="item in presetItems"
:key="item.value"
size="xs"
class="flex-1 justify-center"
:variant="transcodePreset === item.value ? 'solid' : 'ghost'"
:color="transcodePreset === item.value ? 'primary' : 'neutral'"
:label="item.label"
:disabled="!isAdmin"
@click="selectPreset(item.value)"
/>
</div>
</div>
<div
class="flex items-center gap-2 text-sm"
:class="transcodePreset === 'compress' ? '' : 'opacity-40'"
>
<span class="shrink-0">{{ t('preferences.compressBitrate') }}</span>
<input
type="range"
:min="minBitratePercent"
:max="maxBitratePercent"
:value="transcodeBitratePercent"
:disabled="!isAdmin || transcodePreset !== 'compress'"
class="h-2 min-w-24 flex-1 cursor-pointer appearance-none rounded-full bg-default accent-primary disabled:opacity-40"
@input="onBitrateInput"
>
<span class="w-12 shrink-0 text-right tabular-nums text-muted">
{{ transcodeBitratePercent }}%
</span>
</div>
<TranscodeSettingsControls />
</div>
</div>
@@ -0,0 +1,182 @@
<script setup lang="ts">
const props = defineProps<{
currentOrigin: string
effectiveSiteUrl: string
}>()
const siteBaseUrl = defineModel<string>('siteBaseUrl', { required: true })
const imageBaseUrl = defineModel<string>('imageBaseUrl', { required: true })
const videoBaseUrl = defineModel<string>('videoBaseUrl', { required: true })
const fileBaseUrl = defineModel<string>('fileBaseUrl', { required: true })
const imageSeparated = defineModel<boolean>('imageSeparated', { required: true })
const videoSeparated = defineModel<boolean>('videoSeparated', { required: true })
const fileSeparated = defineModel<boolean>('fileSeparated', { required: true })
const { t } = useI18n()
function fillDetected(target: 'site' | 'image' | 'video' | 'file') {
if (!props.currentOrigin) return
if (target === 'site') siteBaseUrl.value = props.currentOrigin
else if (target === 'image') imageBaseUrl.value = props.currentOrigin
else if (target === 'video') videoBaseUrl.value = props.currentOrigin
else fileBaseUrl.value = props.currentOrigin
}
function effectiveUrl(separated: boolean, configuredUrl: string): string {
return separated
? configuredUrl.trim() || props.currentOrigin
: siteBaseUrl.value.trim() || props.effectiveSiteUrl || props.currentOrigin
}
</script>
<template>
<div class="grid gap-4 sm:grid-cols-2">
<SettingsGroup>
<div class="space-y-3 py-4 sm:pr-4">
<p class="text-sm font-medium text-highlighted">
{{ t('settings.siteBaseUrl') }}
</p>
<p class="text-xs leading-relaxed text-muted">
{{ t('settings.siteBaseUrlRoleHint') }}
</p>
<div class="flex gap-2">
<UInput
v-model="siteBaseUrl"
size="sm"
:placeholder="t('settings.siteBaseUrlPlaceholder')"
class="min-w-0 flex-1 font-mono"
/>
<UButton
v-if="currentOrigin"
:label="t('settings.fillDetectedOrigin')"
color="neutral"
variant="outline"
size="sm"
class="shrink-0"
@click="fillDetected('site')"
/>
</div>
<p class="truncate rounded-lg bg-muted/30 px-3 py-2 text-xs text-muted">
{{ t('settings.siteBaseUrlActive', { url: effectiveSiteUrl || currentOrigin }) }}
</p>
</div>
</SettingsGroup>
<SettingsGroup>
<div class="space-y-3 py-4 sm:pl-4">
<div class="flex flex-wrap items-center justify-between gap-2">
<p class="text-sm font-medium text-highlighted">
{{ t('settings.imageBaseUrl') }}
</p>
<USwitch v-model="imageSeparated" />
</div>
<p class="text-xs leading-relaxed text-muted">
{{ t('settings.imageBaseUrlRoleHint') }}
</p>
<div
class="flex gap-2"
:class="imageSeparated ? '' : 'pointer-events-none opacity-50'"
>
<UInput
v-model="imageBaseUrl"
size="sm"
:placeholder="t('settings.imageBaseUrlPlaceholder')"
class="min-w-0 flex-1 font-mono"
/>
<UButton
v-if="currentOrigin"
:label="t('settings.fillDetectedOrigin')"
color="neutral"
variant="outline"
size="sm"
class="shrink-0"
@click="fillDetected('image')"
/>
</div>
<p class="truncate rounded-lg bg-muted/30 px-3 py-2 text-xs text-muted">
{{ t('settings.imageBaseUrlActive', {
url: effectiveUrl(imageSeparated, imageBaseUrl)
}) }}
</p>
</div>
</SettingsGroup>
<SettingsGroup>
<div class="space-y-3 py-4 sm:pr-4">
<div class="flex flex-wrap items-center justify-between gap-2">
<p class="text-sm font-medium text-highlighted">
{{ t('settings.videoBaseUrl') }}
</p>
<USwitch v-model="videoSeparated" />
</div>
<p class="text-xs leading-relaxed text-muted">
{{ t('settings.videoBaseUrlRoleHint') }}
</p>
<div
class="flex gap-2"
:class="videoSeparated ? '' : 'pointer-events-none opacity-50'"
>
<UInput
v-model="videoBaseUrl"
size="sm"
:placeholder="t('settings.videoBaseUrlPlaceholder')"
class="min-w-0 flex-1 font-mono"
/>
<UButton
v-if="currentOrigin"
:label="t('settings.fillDetectedOrigin')"
color="neutral"
variant="outline"
size="sm"
class="shrink-0"
@click="fillDetected('video')"
/>
</div>
<p class="truncate rounded-lg bg-muted/30 px-3 py-2 text-xs text-muted">
{{ t('settings.videoBaseUrlActive', {
url: effectiveUrl(videoSeparated, videoBaseUrl)
}) }}
</p>
</div>
</SettingsGroup>
<SettingsGroup>
<div class="space-y-3 py-4 sm:pl-4">
<div class="flex flex-wrap items-center justify-between gap-2">
<p class="text-sm font-medium text-highlighted">
{{ t('settings.fileBaseUrl') }}
</p>
<USwitch v-model="fileSeparated" />
</div>
<p class="text-xs leading-relaxed text-muted">
{{ t('settings.fileBaseUrlRoleHint') }}
</p>
<div
class="flex gap-2"
:class="fileSeparated ? '' : 'pointer-events-none opacity-50'"
>
<UInput
v-model="fileBaseUrl"
size="sm"
:placeholder="t('settings.fileBaseUrlPlaceholder')"
class="min-w-0 flex-1 font-mono"
/>
<UButton
v-if="currentOrigin"
:label="t('settings.fillDetectedOrigin')"
color="neutral"
variant="outline"
size="sm"
class="shrink-0"
@click="fillDetected('file')"
/>
</div>
<p class="truncate rounded-lg bg-muted/30 px-3 py-2 text-xs text-muted">
{{ t('settings.fileBaseUrlActive', {
url: effectiveUrl(fileSeparated, fileBaseUrl)
}) }}
</p>
</div>
</SettingsGroup>
</div>
</template>
@@ -1,11 +1,14 @@
<script setup lang="ts">
type PolicySource = 'env' | 'db' | 'default'
type MimeType = 'image/jpeg' | 'image/png' | 'image/webp' | 'image/gif' | 'image/svg+xml' | 'image/x-icon'
type VideoExtension = 'mp4' | 'm4v' | 'webm' | 'mov' | 'mkv' | 'avi' | 'flv' | 'wmv' | 'mpg' | 'mpeg'
defineProps<{
const props = defineProps<{
part: 'upload-preference' | 'image-processing' | 'login-rate'
uploadMaxFileSizeMbSource?: PolicySource
allowedMimeTypesSource?: PolicySource
allowedVideoExtensionsSource?: PolicySource
forbiddenFileExtensionsSource?: PolicySource
uploadRateIpMaxSource?: PolicySource
uploadRateTokenMaxSource?: PolicySource
uploadRateWindowMinutesSource?: PolicySource
@@ -13,16 +16,21 @@ defineProps<{
loginRateWindowMinutesSource?: PolicySource
preserveOriginalUploadSource?: PolicySource
mimeTypeOptions?: ReadonlyArray<{ mime: MimeType, label: string }>
videoExtensionOptions?: ReadonlyArray<{ extension: VideoExtension, label: string }>
forbiddenFileExtensionOptions?: ReadonlyArray<{ extension: string, label: string }>
}>()
const uploadMaxFileSizeMbDraft = defineModel<number>('uploadMaxFileSizeMb')
const allowedMimeTypesDraft = defineModel<MimeType[]>('allowedMimeTypes')
const allowedVideoExtensionsDraft = defineModel<VideoExtension[]>('allowedVideoExtensions')
const forbiddenFileExtensionsDraft = defineModel<string[]>('forbiddenFileExtensions')
const uploadRateIpMaxDraft = defineModel<number>('uploadRateIpMax')
const uploadRateTokenMaxDraft = defineModel<number>('uploadRateTokenMax')
const uploadRateWindowMinutesDraft = defineModel<number>('uploadRateWindowMinutes')
const loginRateMaxDraft = defineModel<number>('loginRateMax')
const loginRateWindowMinutesDraft = defineModel<number>('loginRateWindowMinutes')
const preserveOriginalUploadDraft = defineModel<boolean>('preserveOriginalUpload')
const customForbiddenExtension = ref('')
const { t } = useI18n()
@@ -53,6 +61,70 @@ function isMimeChecked(mime: MimeType): boolean {
return allowedMimeTypesDraft.value?.includes(mime) ?? false
}
function toggleVideoExtension(extension: VideoExtension, checked: boolean) {
if (!allowedVideoExtensionsDraft.value) return
if (checked) {
if (!allowedVideoExtensionsDraft.value.includes(extension)) {
allowedVideoExtensionsDraft.value = [
...allowedVideoExtensionsDraft.value,
extension
]
}
return
}
if (allowedVideoExtensionsDraft.value.length <= 1) return
allowedVideoExtensionsDraft.value = allowedVideoExtensionsDraft.value
.filter(item => item !== extension)
}
function isVideoExtensionChecked(extension: VideoExtension): boolean {
return allowedVideoExtensionsDraft.value?.includes(extension) ?? false
}
function toggleForbiddenExtension(extension: string, checked: boolean) {
if (!forbiddenFileExtensionsDraft.value) return
if (checked) {
if (!forbiddenFileExtensionsDraft.value.includes(extension)) {
forbiddenFileExtensionsDraft.value = [
...forbiddenFileExtensionsDraft.value,
extension
]
}
return
}
forbiddenFileExtensionsDraft.value = forbiddenFileExtensionsDraft.value
.filter(item => item !== extension)
}
function isForbiddenExtensionChecked(extension: string): boolean {
return forbiddenFileExtensionsDraft.value?.includes(extension) ?? false
}
const allForbiddenExtensionOptions = computed(() => {
const options = new Map(
(props.forbiddenFileExtensionOptions ?? []).map(option => [
option.extension,
option.label
])
)
for (const extension of forbiddenFileExtensionsDraft.value ?? []) {
if (!options.has(extension)) options.set(extension, extension.toUpperCase())
}
return [...options].map(([extension, label]) => ({ extension, label }))
})
function addCustomForbiddenExtension() {
const values = customForbiddenExtension.value
.split(',')
.map(value => value.trim().toLowerCase().replace(/^\.+/, ''))
.filter(value => /^[a-z0-9][a-z0-9_-]{0,15}$/.test(value))
if (!values.length) return
const next = new Set(forbiddenFileExtensionsDraft.value ?? [])
for (const value of values) next.add(value)
forbiddenFileExtensionsDraft.value = [...next]
customForbiddenExtension.value = ''
}
function clampUploadMaxFileSizeMb(value: unknown): number {
const num = Number(value)
if (!Number.isFinite(num)) return uploadMaxFileSizeMbDraft.value ?? 10
@@ -118,7 +190,7 @@ function onUploadMaxFileSizeNumberInput(value: string | number) {
</div>
</div>
<div class="py-4">
<div class="border-t border-default/50 py-4">
<div class="flex flex-wrap items-center gap-2">
<p class="text-sm font-medium text-highlighted">
{{ t('settings.allowedMimeTypes') }}
@@ -158,6 +230,105 @@ function onUploadMaxFileSizeNumberInput(value: string | number) {
</div>
</div>
<div class="border-t border-default/50 py-4">
<div class="flex flex-wrap items-center gap-2">
<p class="text-sm font-medium text-highlighted">
{{ t('settings.allowedVideoExtensions') }}
</p>
<UBadge
v-if="allowedVideoExtensionsSource"
:color="sourceBadge(allowedVideoExtensionsSource).color"
variant="subtle"
size="xs"
>
{{ sourceBadge(allowedVideoExtensionsSource).label }}
</UBadge>
</div>
<p class="mt-1 text-xs leading-relaxed text-muted">
{{ t('settings.allowedVideoExtensionsHint') }}
</p>
<div
class="mt-3 flex flex-wrap gap-2"
:class="allowedVideoExtensionsSource === 'env' ? 'pointer-events-none opacity-60' : ''"
>
<label
v-for="option in videoExtensionOptions ?? []"
:key="option.extension"
class="inline-flex cursor-pointer items-center gap-2 rounded-lg border px-3 py-2 text-sm transition-colors"
:class="isVideoExtensionChecked(option.extension)
? 'border-primary/40 bg-primary/5'
: 'border-default bg-muted/20 hover:bg-muted/40'"
>
<UCheckbox
:model-value="isVideoExtensionChecked(option.extension)"
@update:model-value="(v) => toggleVideoExtension(option.extension, v === true)"
@click.stop
/>
<span class="font-medium text-highlighted">{{ option.label }}</span>
</label>
</div>
</div>
<div class="border-t border-default/50 py-4">
<div class="flex flex-wrap items-center gap-2">
<p class="text-sm font-medium text-highlighted">
{{ t('settings.forbiddenFileExtensions') }}
</p>
<UBadge
v-if="forbiddenFileExtensionsSource"
:color="sourceBadge(forbiddenFileExtensionsSource).color"
variant="subtle"
size="xs"
>
{{ sourceBadge(forbiddenFileExtensionsSource).label }}
</UBadge>
</div>
<p class="mt-1 text-xs leading-relaxed text-muted">
{{ t('settings.forbiddenFileExtensionsHint') }}
</p>
<div
class="mt-3 flex flex-wrap gap-2"
:class="forbiddenFileExtensionsSource === 'env' ? 'pointer-events-none opacity-60' : ''"
>
<label
v-for="option in allForbiddenExtensionOptions"
:key="option.extension"
class="inline-flex cursor-pointer items-center gap-2 rounded-lg border px-3 py-2 text-sm transition-colors"
:class="isForbiddenExtensionChecked(option.extension)
? 'border-error/40 bg-error/5'
: 'border-default bg-muted/20 hover:bg-muted/40'"
>
<UCheckbox
:model-value="isForbiddenExtensionChecked(option.extension)"
@update:model-value="(v) => toggleForbiddenExtension(option.extension, v === true)"
@click.stop
/>
<span class="font-medium text-highlighted">{{ option.label }}</span>
</label>
</div>
<div
class="mt-3 flex max-w-md items-center gap-2"
:class="forbiddenFileExtensionsSource === 'env' ? 'pointer-events-none opacity-60' : ''"
>
<UInput
v-model="customForbiddenExtension"
size="sm"
class="min-w-0 flex-1"
:placeholder="t('settings.forbiddenFileExtensionsCustomPlaceholder')"
:disabled="forbiddenFileExtensionsSource === 'env'"
@keydown.enter.prevent="addCustomForbiddenExtension"
/>
<UButton
:label="t('settings.forbiddenFileExtensionsAdd')"
size="sm"
variant="outline"
color="neutral"
:disabled="forbiddenFileExtensionsSource === 'env'"
@click="addCustomForbiddenExtension"
/>
</div>
</div>
<div class="py-4">
<p class="text-sm font-medium text-highlighted">
{{ t('settings.uploadRateGroup') }}
@@ -1,7 +1,12 @@
<script setup lang="ts">
defineProps<{
import type { UploadMediaKind } from '~/composables/useUploadPreferences'
const props = withDefaults(defineProps<{
part: 'upload-preference' | 'image-processing'
}>()
kind?: UploadMediaKind
}>(), {
kind: 'image'
})
const {
compressEnabled,
@@ -9,7 +14,7 @@ const {
copyFormat,
clientWebpQuality,
loadPreferences
} = useUploadPreferences()
} = useUploadPreferences(props.kind)
const { t } = useI18n()
@@ -0,0 +1,22 @@
<script setup lang="ts">
const { transcodeToHls, loadPreferences } = useUploadPreferences('video')
const { t } = useI18n()
onMounted(() => {
loadPreferences()
})
</script>
<template>
<SettingsToggleRow
v-model="transcodeToHls"
:title="t('preferences.transcodeToHls')"
:hint="t('preferences.transcodeToHlsHint')"
/>
<div
v-if="transcodeToHls"
class="border-t border-default/50 py-4"
>
<TranscodeSettingsControls />
</div>
</template>