mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-07 00:29:56 +08:00
Merge pull request #409 from stkevintan/download_uri
This commit is contained in:
@@ -1084,6 +1084,8 @@ export interface DownloaderConf {
|
|||||||
config: { [key: string]: any }
|
config: { [key: string]: any }
|
||||||
// 是否启用
|
// 是否启用
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
// 路径映射
|
||||||
|
path_mapping?: Array<[storagePath: string, downloadPath: string]>
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通知配置
|
// 通知配置
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { DownloaderInfo } from '@/api/types'
|
|||||||
import { getLogoUrl } from '@/utils/imageUtils'
|
import { getLogoUrl } from '@/utils/imageUtils'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { downloaderDict } from '@/api/constants'
|
import { downloaderDict, storageAttributes } from '@/api/constants'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
@@ -52,6 +52,56 @@ const download_rate = ref(0)
|
|||||||
// 下载器详情弹窗
|
// 下载器详情弹窗
|
||||||
const downloaderInfoDialog = ref(false)
|
const downloaderInfoDialog = ref(false)
|
||||||
|
|
||||||
|
// 表单
|
||||||
|
const downloaderForm = ref()
|
||||||
|
|
||||||
|
// 路径前缀选项
|
||||||
|
const prefixOptions = computed(() => {
|
||||||
|
return storageAttributes.map(item => ({
|
||||||
|
title: t(`storage.${item.type}`),
|
||||||
|
value: item.type
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
function getStorageType(path: string) {
|
||||||
|
if (!path) return 'local'
|
||||||
|
// 查找匹配的存储类型
|
||||||
|
const storage = storageAttributes.find(
|
||||||
|
s => s.type !== 'local' && path.startsWith(`${s.type}:`)
|
||||||
|
)
|
||||||
|
return storage?.type || 'local'
|
||||||
|
}
|
||||||
|
|
||||||
|
function storage2Prefix(storage: string) {
|
||||||
|
return storage === 'local' ? '' : storage + ':'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取存储路径前后缀
|
||||||
|
function parseStoragePath(path: string): [prefix: string, suffix: string] {
|
||||||
|
if (!path) return ['', '']
|
||||||
|
const storage = getStorageType(path)
|
||||||
|
const prefix = storage2Prefix(storage)
|
||||||
|
return [prefix, path.slice(prefix.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新存储路径前缀
|
||||||
|
function updateStoragePrefix(row: PathMappingRow, storage: string) {
|
||||||
|
const [, currentSuffix] = parseStoragePath(row.storage)
|
||||||
|
const prefix = storage2Prefix(storage)
|
||||||
|
row.storage = prefix + currentSuffix
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新存储路径后缀
|
||||||
|
function updateStorageSuffix(row: PathMappingRow, suffix: string) {
|
||||||
|
const [currentPrefix] = parseStoragePath(row.storage)
|
||||||
|
row.storage = currentPrefix + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathValidationRules = [
|
||||||
|
(v: string) => !!v || t('downloader.pathMappingRequired'),
|
||||||
|
(v: string) => v.startsWith('/') || t('downloader.pathMappingError'),
|
||||||
|
]
|
||||||
|
|
||||||
// 下载器详情
|
// 下载器详情
|
||||||
const downloaderInfo = ref<DownloaderConf>({
|
const downloaderInfo = ref<DownloaderConf>({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -59,8 +109,24 @@ const downloaderInfo = ref<DownloaderConf>({
|
|||||||
default: false,
|
default: false,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
config: {},
|
config: {},
|
||||||
|
path_mapping: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 路径映射行定义
|
||||||
|
interface PathMappingRow {
|
||||||
|
id: string
|
||||||
|
storage: string
|
||||||
|
download: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路径映射行数据
|
||||||
|
const pathMappingRows = ref<PathMappingRow[]>([])
|
||||||
|
|
||||||
|
// 生成随机ID
|
||||||
|
function generateId() {
|
||||||
|
return Math.random().toString(36).substring(2, 9)
|
||||||
|
}
|
||||||
|
|
||||||
// 下载器是否应该刷新数据的计算属性
|
// 下载器是否应该刷新数据的计算属性
|
||||||
const shouldRefresh = computed(() => props.allowRefresh && props.downloader.enabled)
|
const shouldRefresh = computed(() => props.allowRefresh && props.downloader.enabled)
|
||||||
|
|
||||||
@@ -92,11 +158,24 @@ async function loadDownloaderInfo() {
|
|||||||
function openDownloaderInfoDialog() {
|
function openDownloaderInfoDialog() {
|
||||||
// 深复制
|
// 深复制
|
||||||
downloaderInfo.value = cloneDeep(props.downloader)
|
downloaderInfo.value = cloneDeep(props.downloader)
|
||||||
|
// 初始化路径映射行数据
|
||||||
|
pathMappingRows.value = (downloaderInfo.value.path_mapping || []).map(item => ({
|
||||||
|
id: generateId(),
|
||||||
|
storage: item[0],
|
||||||
|
download: item[1],
|
||||||
|
}))
|
||||||
downloaderInfoDialog.value = true
|
downloaderInfoDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存详情数据
|
// 保存详情数据
|
||||||
function saveDownloaderInfo() {
|
async function saveDownloaderInfo() {
|
||||||
|
// 表单校验
|
||||||
|
const { valid } = await downloaderForm.value?.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
// 同步路径映射数据
|
||||||
|
downloaderInfo.value.path_mapping = pathMappingRows.value.map(row => [row.storage, row.download])
|
||||||
|
|
||||||
// 为空不保存,跳出警告框
|
// 为空不保存,跳出警告框
|
||||||
if (!downloaderInfo.value.name) {
|
if (!downloaderInfo.value.name) {
|
||||||
$toast.error(t('downloader.nameRequired'))
|
$toast.error(t('downloader.nameRequired'))
|
||||||
@@ -134,6 +213,20 @@ const getIcon = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 添加路径映射
|
||||||
|
function addPathMapping() {
|
||||||
|
pathMappingRows.value.push({
|
||||||
|
id: generateId(),
|
||||||
|
storage: '',
|
||||||
|
download: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除路径映射
|
||||||
|
function removePathMapping(index: number) {
|
||||||
|
pathMappingRows.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
// 按钮点击
|
// 按钮点击
|
||||||
function onClose() {
|
function onClose() {
|
||||||
emit('close')
|
emit('close')
|
||||||
@@ -152,6 +245,38 @@ onUnmounted(() => {
|
|||||||
stopRefresh()
|
stopRefresh()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.pm-row {
|
||||||
|
padding-inline-start: 12px;
|
||||||
|
padding-inline-end: 12px;
|
||||||
|
:deep(.v-input__append) {
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rp-select {
|
||||||
|
flex: 0 0 95px;
|
||||||
|
margin-right: -1px;
|
||||||
|
:deep(.v-field) {
|
||||||
|
padding-inline-end: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
--v-field-padding-end: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rp-input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
:deep(.v-input__prepend) {
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
& > :deep(.v-input__control .v-field) {
|
||||||
|
padding-inline-start: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<VHover v-slot="hover">
|
<VHover v-slot="hover">
|
||||||
@@ -212,7 +337,7 @@ onUnmounted(() => {
|
|||||||
<VDialogCloseBtn v-model="downloaderInfoDialog" />
|
<VDialogCloseBtn v-model="downloaderInfoDialog" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VForm>
|
<VForm ref="downloaderForm">
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VSwitch v-model="downloaderInfo.enabled" :label="t('downloader.enabled')" />
|
<VSwitch v-model="downloaderInfo.enabled" :label="t('downloader.enabled')" />
|
||||||
@@ -373,6 +498,58 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VLabel class="mb-2">{{ t('downloader.pathMapping') }}</VLabel>
|
||||||
|
<VRow
|
||||||
|
v-for="(row, index) in pathMappingRows"
|
||||||
|
:key="row.id"
|
||||||
|
class="align-start flex-wrap pm-row"
|
||||||
|
>
|
||||||
|
<VCol cols="12" md="6" class="pl-0 pr-0">
|
||||||
|
<div class="d-flex flex-nowrap align-start">
|
||||||
|
<VTextField
|
||||||
|
class="rp-input"
|
||||||
|
:model-value="parseStoragePath(row.storage)[1]"
|
||||||
|
:placeholder="t('downloader.storagePath')"
|
||||||
|
density="compact"
|
||||||
|
hide-details="auto"
|
||||||
|
:rules="pathValidationRules"
|
||||||
|
@update:model-value="v => updateStorageSuffix(row, v)"
|
||||||
|
append-icon="mdi-arrow-right"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VSelect
|
||||||
|
class="rp-select"
|
||||||
|
:model-value="getStorageType(row.storage)"
|
||||||
|
:items="prefixOptions"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="v => updateStoragePrefix(row, v)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VTextField>
|
||||||
|
</div>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6" class="pl-0 pr-0">
|
||||||
|
<VTextField
|
||||||
|
v-model="row.download"
|
||||||
|
:placeholder="t('downloader.downloadPath')"
|
||||||
|
density="compact"
|
||||||
|
hide-details="auto"
|
||||||
|
:rules="pathValidationRules"
|
||||||
|
append-icon="mdi-close"
|
||||||
|
@click:append="removePathMapping(index)"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCol>
|
||||||
|
<VCol>
|
||||||
|
<VBtn variant="tonal" size="small" prepend-icon="mdi-plus" @click="addPathMapping">
|
||||||
|
{{ t('common.add') }}
|
||||||
|
</VBtn>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions class="pt-3">
|
<VCardActions class="pt-3">
|
||||||
|
|||||||
@@ -2676,6 +2676,11 @@ export default {
|
|||||||
hostRequired: 'Host cannot be empty',
|
hostRequired: 'Host cannot be empty',
|
||||||
usernameRequired: 'Username cannot be empty',
|
usernameRequired: 'Username cannot be empty',
|
||||||
passwordRequired: 'Password cannot be empty',
|
passwordRequired: 'Password cannot be empty',
|
||||||
|
pathMapping: 'Path Mapping',
|
||||||
|
pathMappingRequired: 'Path cannot be empty',
|
||||||
|
pathMappingError: 'Must start with /',
|
||||||
|
storagePath: 'Storage Path',
|
||||||
|
downloadPath: 'Download Path',
|
||||||
},
|
},
|
||||||
filterRule: {
|
filterRule: {
|
||||||
title: 'Filter Rule',
|
title: 'Filter Rule',
|
||||||
|
|||||||
@@ -2644,6 +2644,11 @@ export default {
|
|||||||
hostRequired: '地址不能为空',
|
hostRequired: '地址不能为空',
|
||||||
usernameRequired: '用户名不能为空',
|
usernameRequired: '用户名不能为空',
|
||||||
passwordRequired: '密码不能为空',
|
passwordRequired: '密码不能为空',
|
||||||
|
pathMapping: '路径映射',
|
||||||
|
pathMappingRequired: '路径不能为空',
|
||||||
|
pathMappingError: '必须以 / 开头',
|
||||||
|
storagePath: '存储路径',
|
||||||
|
downloadPath: '下载路径',
|
||||||
},
|
},
|
||||||
filterRule: {
|
filterRule: {
|
||||||
title: '过滤规则',
|
title: '过滤规则',
|
||||||
|
|||||||
@@ -2630,6 +2630,11 @@ export default {
|
|||||||
hostRequired: '地址不能為空',
|
hostRequired: '地址不能為空',
|
||||||
usernameRequired: '用戶名不能為空',
|
usernameRequired: '用戶名不能為空',
|
||||||
passwordRequired: '密碼不能為空',
|
passwordRequired: '密碼不能為空',
|
||||||
|
pathMapping: '路徑映射',
|
||||||
|
pathMappingRequired: '路徑不能為空',
|
||||||
|
pathMappingError: '必須以 / 開頭',
|
||||||
|
storagePath: '存儲路徑',
|
||||||
|
downloadPath: '下載路徑',
|
||||||
},
|
},
|
||||||
filterRule: {
|
filterRule: {
|
||||||
title: '過濾規則',
|
title: '過濾規則',
|
||||||
|
|||||||
Reference in New Issue
Block a user