添加下载器和媒体服务器选项,重构相关组件以支持新功能,并更新国际化文本以提升用户体验

This commit is contained in:
jxxghp
2025-05-02 08:01:19 +08:00
parent bf22d7f5e9
commit 5f2e93dde3
9 changed files with 108 additions and 41 deletions
+40 -5
View File
@@ -26,11 +26,6 @@ export const storageAttributes = [
icon: 'mdi-server-network-outline', icon: 'mdi-server-network-outline',
remote: true, remote: true,
}, },
{
type: 'custom',
icon: 'mdi-database',
remote: true,
},
] ]
export const storageIconDict = storageAttributes.reduce((dict, item) => { export const storageIconDict = storageAttributes.reduce((dict, item) => {
@@ -43,6 +38,46 @@ export const storageRemoteDict = storageAttributes.reduce((dict, item) => {
return dict return dict
}, {} as Record<string, boolean>) }, {} as Record<string, boolean>)
export const downloaderOptions = [
{
value: 'qbittorrent',
title: i18n.global.t('setting.system.qbittorrent'),
},
{
value: 'transmission',
title: i18n.global.t('setting.system.transmission'),
},
]
export const downloaderDict = downloaderOptions.reduce((dict, item) => {
dict[item.value] = item.title
return dict
}, {} as Record<string, string>)
export const mediaServerOptions = [
{
value: 'emby',
title: i18n.global.t('setting.system.emby'),
},
{
value: 'jellyfin',
title: i18n.global.t('setting.system.jellyfin'),
},
{
value: 'plex',
title: i18n.global.t('setting.system.plex'),
},
{
value: 'trimemedia',
title: i18n.global.t('setting.system.trimeMedia'),
},
]
export const mediaServerDict = mediaServerOptions.reduce((dict, item) => {
dict[item.value] = item.title
return dict
}, {} as Record<string, string>)
export const innerFilterRules = [ export const innerFilterRules = [
{ title: i18n.global.t('filterRules.specSub'), value: ' SPECSUB ' }, { title: i18n.global.t('filterRules.specSub'), value: ' SPECSUB ' },
{ title: i18n.global.t('filterRules.cnSub'), value: ' CNSUB ' }, { title: i18n.global.t('filterRules.cnSub'), value: ' CNSUB ' },
+2 -2
View File
@@ -33,8 +33,8 @@ const isCollapsed = ref(true)
// 类型下拉字典 // 类型下拉字典
const typeItems = computed(() => [ const typeItems = computed(() => [
{ title: t('common.all'), value: '' }, { title: t('common.all'), value: '' },
{ title: t('media.movie'), value: '电影' }, { title: t('mediaType.movie'), value: '电影' },
{ title: t('media.tv'), value: '电视剧' }, { title: t('mediaType.tv'), value: '电视剧' },
]) ])
// 计算资源存储字典(整理方式为下载器时不能为远程存储) // 计算资源存储字典(整理方式为下载器时不能为远程存储)
+13 -3
View File
@@ -9,6 +9,7 @@ import transmission_image from '@images/logos/transmission.png'
import custom_image from '@images/logos/downloader.png' import custom_image from '@images/logos/downloader.png'
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'
// 获取i18n实例 // 获取i18n实例
const { t } = useI18n() const { t } = useI18n()
@@ -173,11 +174,11 @@ onUnmounted(() => {
/> />
<span class="text-h6">{{ downloader.name }}</span> <span class="text-h6">{{ downloader.name }}</span>
</div> </div>
<div v-if="downloader.type != 'custom' && props.downloader.enabled" class="mt-1 flex flex-wrap text-sm"> <div v-if="downloaderDict[downloader.type] && props.downloader.enabled" class="mt-1 flex flex-wrap text-sm">
<span class="me-2">{{ `${formatFileSize(upload_rate, 1)}/s ` }}</span> <span class="me-2">{{ `${formatFileSize(upload_rate, 1)}/s ` }}</span>
<span>{{ `${formatFileSize(download_rate, 1)}/s` }}</span> <span>{{ `${formatFileSize(download_rate, 1)}/s` }}</span>
</div> </div>
<div v-else-if="downloader.type == 'custom'" class="mt-1 flex flex-wrap text-sm"> <div v-else-if="!downloaderDict[downloader.type]" class="mt-1 flex flex-wrap text-sm">
<span class="me-2">自定义下载器</span> <span class="me-2">自定义下载器</span>
</div> </div>
</div> </div>
@@ -324,11 +325,20 @@ onUnmounted(() => {
</VCol> </VCol>
</VRow> </VRow>
<VRow v-else> <VRow v-else>
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.type"
:label="t('downloader.type')"
:hint="t('downloader.customTypeHint')"
persistent-hint
active
/>
</VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="downloaderInfo.name" v-model="downloaderInfo.name"
:label="t('downloader.name')" :label="t('downloader.name')"
:placeholder="t('downloader.nameRequired')" :hint="t('downloader.nameRequired')"
persistent-hint persistent-hint
active active
/> />
+10 -5
View File
@@ -9,6 +9,7 @@ import custom_image from '@images/logos/mediaserver.png'
import api from '@/api' import api from '@/api'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { mediaServerDict } from '@/api/constants'
// 获取i18n实例 // 获取i18n实例
const { t } = useI18n() const { t } = useI18n()
@@ -186,12 +187,12 @@ onMounted(() => {
<VCardText class="flex justify-space-between align-center gap-3"> <VCardText class="flex justify-space-between align-center gap-3">
<div class="align-self-start flex-1"> <div class="align-self-start flex-1">
<div class="text-h6 mb-1">{{ mediaserver.name }}</div> <div class="text-h6 mb-1">{{ mediaserver.name }}</div>
<div v-if="mediaserver.type != 'custom' && mediaserver.enabled" class="text-sm mt-5 flex flex-wrap"> <div v-if="mediaServerDict[mediaserver.type] && mediaserver.enabled" class="text-sm mt-5 flex flex-wrap">
<span v-for="item in infoItems" :key="item.title" class="me-2 mb-1"> <span v-for="item in infoItems" :key="item.title" class="me-2 mb-1">
<VIcon rounded :icon="item.avatar" class="me-1" />{{ item.amount }} <VIcon rounded :icon="item.avatar" class="me-1" />{{ item.amount }}
</span> </span>
</div> </div>
<div v-else-if="mediaserver.type == 'custom'" class="text-sm mt-5 flex flex-wrap"> <div v-else-if="!mediaServerDict[mediaserver.type]" class="text-sm mt-5 flex flex-wrap">
<span class="me-2 mb-1">自定义媒体服务器</span> <span class="me-2 mb-1">自定义媒体服务器</span>
</div> </div>
</div> </div>
@@ -453,11 +454,15 @@ onMounted(() => {
<VRow v-else> <VRow v-else>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="mediaServerInfo.name" v-model="mediaServerInfo.type"
:label="t('common.name')" :label="t('mediaserver.type')"
:placeholder="t('mediaserver.nameRequired')" :hint="t('mediaserver.customTypeHint')"
persistent-hint
/> />
</VCol> </VCol>
<VCol cols="12" md="6">
<VTextField :label="t('common.name')" :hint="t('mediaserver.nameRequired')" persistent-hint />
</VCol>
</VRow> </VRow>
</VForm> </VForm>
</VCardText> </VCardText>
+21 -8
View File
@@ -15,6 +15,7 @@ import AlistConfigDialog from '../dialog/AlistConfigDialog.vue'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import { isNullOrEmptyObject } from '@/@core/utils' import { isNullOrEmptyObject } from '@/@core/utils'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { storageIconDict } from '@/api/constants'
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
@@ -50,6 +51,9 @@ const storage_ref = ref(props.storage)
// 自定义存储名称 // 自定义存储名称
const customName = ref(props.storage.name) const customName = ref(props.storage.name)
// 自定义存储类型
const storageType = ref(props.storage.type)
// 阿里云盘认证对话框 // 阿里云盘认证对话框
const aliyunAuthDialog = ref(false) const aliyunAuthDialog = ref(false)
// 115网盘认证对话框 // 115网盘认证对话框
@@ -76,11 +80,11 @@ function openStorageDialog() {
case 'alist': case 'alist':
aListConfigDialog.value = true aListConfigDialog.value = true
break break
case 'custom': case 'local':
customConfigDialog.value = true $toast.info(t('storage.noConfigNeeded'))
break break
default: default:
$toast.info(t('storage.noConfigNeeded')) customConfigDialog.value = true
break break
} }
} }
@@ -98,10 +102,8 @@ const getIcon = computed(() => {
return rclone_png return rclone_png
case 'alist': case 'alist':
return alist_png return alist_png
case 'custom':
return custom_png
default: default:
return storage_png return custom_png
} }
}) })
@@ -139,7 +141,9 @@ function handleDone() {
rcloneConfigDialog.value = false rcloneConfigDialog.value = false
aListConfigDialog.value = false aListConfigDialog.value = false
customConfigDialog.value = false customConfigDialog.value = false
// 更新存储
storage_ref.value.name = customName.value storage_ref.value.name = customName.value
storage_ref.value.type = storageType.value
emit('done', storage_ref.value) emit('done', storage_ref.value)
} }
@@ -155,11 +159,11 @@ function onClose() {
<template> <template>
<div> <div>
<VCard variant="tonal" @click="openStorageDialog"> <VCard variant="tonal" @click="openStorageDialog">
<VDialogCloseBtn v-if="storage.type == 'custom'" @click="onClose" /> <VDialogCloseBtn v-if="storageIconDict[storage.type]" @click="onClose" />
<VCardText class="flex justify-space-between align-center gap-3"> <VCardText class="flex justify-space-between align-center gap-3">
<div class="align-self-start flex-1"> <div class="align-self-start flex-1">
<h5 class="text-h6 mb-1">{{ storage.name }}</h5> <h5 class="text-h6 mb-1">{{ storage.name }}</h5>
<template v-if="storage.type != 'custom'"> <template v-if="storageIconDict[storage.type]">
<div class="mb-3 text-sm" v-if="total">{{ formatBytes(used, 1) }} / {{ formatBytes(total, 1) }}</div> <div class="mb-3 text-sm" v-if="total">{{ formatBytes(used, 1) }} / {{ formatBytes(total, 1) }}</div>
<div v-else-if="isNullOrEmptyObject(storage.config)">{{ t('storage.notConfigured') }}</div> <div v-else-if="isNullOrEmptyObject(storage.config)">{{ t('storage.notConfigured') }}</div>
</template> </template>
@@ -210,6 +214,15 @@ function onClose() {
<VDivider /> <VDivider />
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12" md="6">
<VTextField
v-model="storageType"
:label="t('storage.type')"
:hint="t('storage.customTypeHint')"
persistent-hint
active
/>
</VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="customName" :label="t('storage.name')" persistent-hint active /> <VTextField v-model="customName" :label="t('storage.name')" persistent-hint active />
</VCol> </VCol>
+5
View File
@@ -728,6 +728,8 @@ export default {
}, },
storage: { storage: {
name: 'Name', name: 'Name',
type: 'Type',
customTypeHint: 'Custom storage type, used for plugins and other scenarios',
usedPercent: '{percent}% Used', usedPercent: '{percent}% Used',
noConfigNeeded: 'This storage type does not require configuration, please configure the directory directly!', noConfigNeeded: 'This storage type does not require configuration, please configure the directory directly!',
notConfigured: 'Not Configured', notConfigured: 'Not Configured',
@@ -2085,6 +2087,7 @@ export default {
name: 'Name', name: 'Name',
type: 'Type', type: 'Type',
enabled: 'Enabled', enabled: 'Enabled',
customTypeHint: 'Custom downloader type, for plugin scenarios',
default: 'Default', default: 'Default',
host: 'Host', host: 'Host',
username: 'Username', username: 'Username',
@@ -2125,6 +2128,8 @@ export default {
}, },
}, },
mediaserver: { mediaserver: {
type: 'Type',
customTypeHint: 'Custom media server type, for plugin scenarios',
enableMediaServer: 'Enable Media Server', enableMediaServer: 'Enable Media Server',
nameRequired: 'Required; cannot be duplicated', nameRequired: 'Required; cannot be duplicated',
serverAlias: 'Media server alias', serverAlias: 'Media server alias',
+7 -2
View File
@@ -725,6 +725,8 @@ export default {
}, },
storage: { storage: {
name: '名称', name: '名称',
type: '类型',
customTypeHint: '自定义存储类型,用于插件等场景',
usedPercent: '已使用 {percent}%', usedPercent: '已使用 {percent}%',
noConfigNeeded: '此存储类型无需配置参数,请直接配置目录!', noConfigNeeded: '此存储类型无需配置参数,请直接配置目录!',
notConfigured: '未配置', notConfigured: '未配置',
@@ -2061,6 +2063,7 @@ export default {
name: '名称', name: '名称',
type: '类型', type: '类型',
enabled: '启用', enabled: '启用',
customTypeHint: '自定义下载器类型,用于插件等场景',
default: '默认', default: '默认',
host: '地址', host: '地址',
username: '用户名', username: '用户名',
@@ -2071,7 +2074,7 @@ export default {
first_last_piece: '优先首尾文件', first_last_piece: '优先首尾文件',
saveSuccess: '下载器设置保存成功', saveSuccess: '下载器设置保存成功',
saveFailed: '下载器设置保存失败', saveFailed: '下载器设置保存失败',
nameRequired: '名称不能为空', nameRequired: '不能为空,且不能重名',
nameDuplicate: '名称已存在', nameDuplicate: '名称已存在',
defaultChanged: '存在默认下载器,已替换', defaultChanged: '存在默认下载器,已替换',
}, },
@@ -2101,8 +2104,10 @@ export default {
}, },
}, },
mediaserver: { mediaserver: {
type: '类型',
customTypeHint: '自定义媒体服务器类型,用于插件等场景',
enableMediaServer: '启用媒体服务器', enableMediaServer: '启用媒体服务器',
nameRequired: '必填不可与其他名称重名', nameRequired: '必填不可重名',
serverAlias: '媒体服务器的别名', serverAlias: '媒体服务器的别名',
host: '地址', host: '地址',
hostPlaceholder: 'http(s)://ip:port', hostPlaceholder: 'http(s)://ip:port',
+5
View File
@@ -726,6 +726,8 @@ export default {
}, },
storage: { storage: {
name: '名稱', name: '名稱',
type: '類型',
customTypeHint: '自定義存儲類型,用於插件等場景',
usedPercent: '已使用 {percent}%', usedPercent: '已使用 {percent}%',
noConfigNeeded: '此存儲類型無需配置參數,請直接配置目錄!', noConfigNeeded: '此存儲類型無需配置參數,請直接配置目錄!',
notConfigured: '未配置', notConfigured: '未配置',
@@ -2062,6 +2064,7 @@ export default {
title: '下載器', title: '下載器',
name: '名稱', name: '名稱',
type: '類型', type: '類型',
customTypeHint: '自定義下載器類型,用於插件等場景',
enabled: '啟用', enabled: '啟用',
default: '預設', default: '預設',
host: '地址', host: '地址',
@@ -2103,6 +2106,8 @@ export default {
}, },
}, },
mediaserver: { mediaserver: {
type: '類型',
customTypeHint: '自定義媒體伺服器類型,用於插件等場景',
enableMediaServer: '啟用媒體伺服器', enableMediaServer: '啟用媒體伺服器',
nameRequired: '必填;不可與其他名稱重名', nameRequired: '必填;不可與其他名稱重名',
serverAlias: '媒體伺服器的別名', serverAlias: '媒體伺服器的別名',
+5 -16
View File
@@ -10,6 +10,7 @@ import MediaServerCard from '@/components/cards/MediaServerCard.vue'
import { copyToClipboard } from '@/@core/utils/navigator' import { copyToClipboard } from '@/@core/utils/navigator'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue' import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { downloaderOptions, mediaServerOptions } from '@/api/constants'
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
@@ -535,11 +536,8 @@ onDeactivated(() => {
<VIcon icon="mdi-plus" /> <VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click> <VMenu activator="parent" close-on-content-click>
<VList> <VList>
<VListItem @click="addDownloader('qbittorrent')"> <VListItem v-for="item in downloaderOptions" @click="addDownloader(item.value)">
<VListItemTitle>{{ t('setting.system.qbittorrent') }}</VListItemTitle> <VListItemTitle>{{ item.title }}</VListItemTitle>
</VListItem>
<VListItem @click="addDownloader('transmission')">
<VListItemTitle>{{ t('setting.system.transmission') }}</VListItemTitle>
</VListItem> </VListItem>
<VListItem @click="addDownloader('custom')"> <VListItem @click="addDownloader('custom')">
<VListItemTitle>{{ t('setting.system.custom') }}</VListItemTitle> <VListItemTitle>{{ t('setting.system.custom') }}</VListItemTitle>
@@ -586,17 +584,8 @@ onDeactivated(() => {
<VIcon icon="mdi-plus" /> <VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click> <VMenu activator="parent" close-on-content-click>
<VList> <VList>
<VListItem @click="addMediaServer('emby')"> <VListItem v-for="item in mediaServerOptions" @click="addMediaServer(item.value)">
<VListItemTitle>{{ t('setting.system.emby') }}</VListItemTitle> <VListItemTitle>{{ item.title }}</VListItemTitle>
</VListItem>
<VListItem @click="addMediaServer('jellyfin')">
<VListItemTitle>{{ t('setting.system.jellyfin') }}</VListItemTitle>
</VListItem>
<VListItem @click="addMediaServer('plex')">
<VListItemTitle>{{ t('setting.system.plex') }}</VListItemTitle>
</VListItem>
<VListItem @click="addMediaServer('trimemedia')">
<VListItemTitle>{{ t('setting.system.trimeMedia') }}</VListItemTitle>
</VListItem> </VListItem>
<VListItem @click="addMediaServer('custom')"> <VListItem @click="addMediaServer('custom')">
<VListItemTitle>{{ t('setting.system.custom') }}</VListItemTitle> <VListItemTitle>{{ t('setting.system.custom') }}</VListItemTitle>