feat: 完成绿联媒体服务前端接入与展示优化

This commit is contained in:
doumao
2026-02-28 22:09:09 +08:00
parent 351faf2891
commit ff7658b5ba
14 changed files with 960 additions and 331 deletions

View File

@@ -80,6 +80,10 @@ export const mediaServerOptions = [
value: 'trimemedia', value: 'trimemedia',
title: i18n.global.t('setting.system.trimeMedia'), title: i18n.global.t('setting.system.trimeMedia'),
}, },
{
value: 'ugreen',
title: i18n.global.t('setting.system.ugreen'),
},
] ]
export const mediaServerDict = mediaServerOptions.reduce((dict, item) => { export const mediaServerDict = mediaServerOptions.reduce((dict, item) => {

View File

@@ -885,8 +885,8 @@ export interface MediaStatistic {
movie_count: number movie_count: number
// 电视剧总数 // 电视剧总数
tv_count: number tv_count: number
// 电视剧总集数 // 电视剧总集数,未获取时为 null
episode_count: number episode_count: number | null
// 用户数量 // 用户数量
user_count: number user_count: number
} }
@@ -1134,7 +1134,7 @@ export interface StorageConf {
export interface MediaServerConf { export interface MediaServerConf {
// 名称 // 名称
name: string name: string
// 类型 emby/jellyfin/plex // 类型 emby/jellyfin/plex/trimemedia/ugreen
type: string type: string
// 配置 // 配置
config: { [key: string]: any } config: { [key: string]: any }

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { MediaServerPlayItem } from '@/api/types' import type { MediaServerPlayItem } from '@/api/types'
import noImage from '@images/no-image.jpeg'
import { openMediaServerWithAutoDetect } from '@/utils/appDeepLink' import { openMediaServerWithAutoDetect } from '@/utils/appDeepLink'
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
@@ -10,12 +11,18 @@ const props = defineProps({
// 图片是否加载完成 // 图片是否加载完成
const imageLoaded = ref(false) const imageLoaded = ref(false)
const imageLoadError = ref(false)
// 图片加载完成响应 // 图片加载完成响应
function imageLoadHandler() { function imageLoadHandler() {
imageLoaded.value = true imageLoaded.value = true
} }
// 图片加载失败响应
function imageErrorHandler() {
imageLoadError.value = true
}
// 跳转播放 // 跳转播放
async function goPlay() { async function goPlay() {
if (props.media?.link) { if (props.media?.link) {
@@ -26,6 +33,7 @@ async function goPlay() {
// 计算图片地址 // 计算图片地址
const getImgUrl = computed(() => { const getImgUrl = computed(() => {
const image = props.media?.image || '' const image = props.media?.image || ''
if (!image || imageLoadError.value) return noImage
let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}` let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
const use_cookies = props.media?.use_cookies const use_cookies = props.media?.use_cookies
if (use_cookies) { if (use_cookies) {
@@ -50,7 +58,7 @@ const getImgUrl = computed(() => {
@click="goPlay" @click="goPlay"
> >
<template #image> <template #image>
<VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler"> <VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
<template #placeholder> <template #placeholder>
<div class="w-full h-full"> <div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" /> <VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />

View File

@@ -33,6 +33,7 @@ function imageLoadHandler() {
// 图片加载错误 // 图片加载错误
function imageErrorHandler() { function imageErrorHandler() {
imageError.value = true imageError.value = true
imgUrl.value = getDefaultImage()
} }
// 默认图片 // 默认图片
@@ -41,6 +42,7 @@ function getDefaultImage() {
else if (props.media?.server_type === 'emby') return emby else if (props.media?.server_type === 'emby') return emby
else if (props.media?.server_type === 'jellyfin') return jellyfin else if (props.media?.server_type === 'jellyfin') return jellyfin
else if (props.media?.server_type === 'trimemedia') return getLogoUrl('trimemedia') else if (props.media?.server_type === 'trimemedia') return getLogoUrl('trimemedia')
else if (props.media?.server_type === 'ugreen') return getLogoUrl('ugreen')
else return plex else return plex
} }
@@ -53,7 +55,7 @@ async function goPlay() {
// 生成图片代理路径 // 生成图片代理路径
function getImgUrl(url: string, use_cookies?: boolean) { function getImgUrl(url: string, use_cookies?: boolean) {
if (!url) return getDefaultImage() if (!url || imageError.value) return getDefaultImage()
let imgurl = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}` let imgurl = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
if (use_cookies) { if (use_cookies) {
imgurl += `&use_cookies=${encodeURIComponent(use_cookies)}` imgurl += `&use_cookies=${encodeURIComponent(use_cookies)}`
@@ -64,7 +66,7 @@ function getImgUrl(url: string, use_cookies?: boolean) {
// 根据多张图片生成媒体库封面 // 根据多张图片生成媒体库封面
async function drawImages(imageList: string[], use_cookies?: boolean) { async function drawImages(imageList: string[], use_cookies?: boolean) {
// 图片 // 图片
const IMAGES = imageList const IMAGES = [...imageList]
if (IMAGES.length === 0) return getDefaultImage() if (IMAGES.length === 0) return getDefaultImage()
// 为所有图片添加system/img前缀 // 为所有图片添加system/img前缀

View File

@@ -61,6 +61,12 @@ const librariesOptions = ref<{ title: string; value: string | undefined }[]>([
}, },
]) ])
const ugreenScanModeOptions = computed(() => [
{ title: t('mediaserver.scanModeOptions.newAndModified'), value: 'new_and_modified' },
{ title: t('mediaserver.scanModeOptions.supplementMissing'), value: 'supplement_missing' },
{ title: t('mediaserver.scanModeOptions.fullOverride'), value: 'full_override' },
])
// 媒体服务器详情弹窗 // 媒体服务器详情弹窗
const mediaServerInfoDialog = ref(false) const mediaServerInfoDialog = ref(false)
@@ -77,6 +83,12 @@ function openMediaServerInfoDialog() {
loadLibrary(props.mediaserver.name) loadLibrary(props.mediaserver.name)
// 深复制 // 深复制
mediaServerInfo.value = cloneDeep(props.mediaserver) mediaServerInfo.value = cloneDeep(props.mediaserver)
if (mediaServerInfo.value.type === 'ugreen') {
mediaServerInfo.value.config = mediaServerInfo.value.config || {}
}
if (mediaServerInfo.value.type === 'ugreen' && !mediaServerInfo.value.config.scan_mode) {
mediaServerInfo.value.config.scan_mode = 'supplement_missing'
}
mediaServerInfoDialog.value = true mediaServerInfoDialog.value = true
if (!props.mediaserver.sync_libraries) { if (!props.mediaserver.sync_libraries) {
mediaServerInfo.value.sync_libraries = ['all'] mediaServerInfo.value.sync_libraries = ['all']
@@ -110,6 +122,8 @@ const getIcon = computed(() => {
return getLogoUrl('jellyfin') return getLogoUrl('jellyfin')
case 'trimemedia': case 'trimemedia':
return getLogoUrl('trimemedia') return getLogoUrl('trimemedia')
case 'ugreen':
return getLogoUrl('ugreen')
case 'plex': case 'plex':
return getLogoUrl('plex') return getLogoUrl('plex')
default: default:
@@ -424,6 +438,85 @@ onMounted(() => {
/> />
</VCol> </VCol>
</VRow> </VRow>
<VRow v-else-if="mediaServerInfo.type == 'ugreen'">
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.name"
:label="t('common.name')"
:placeholder="t('mediaserver.nameRequired')"
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.host"
:label="t('mediaserver.host')"
:placeholder="t('mediaserver.hostPlaceholder')"
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="mediaServerInfo.config.play_host"
:label="t('mediaserver.playHost')"
:placeholder="t('mediaserver.playHostPlaceholder')"
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerInfo.config.username"
:label="t('mediaserver.username')"
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
type="password"
v-model="mediaServerInfo.config.password"
:label="t('mediaserver.password')"
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12">
<VAutocomplete
v-model="mediaServerInfo.sync_libraries"
:label="t('mediaserver.syncLibraries')"
:items="librariesOptions"
chips
multiple
clearable
:hint="t('mediaserver.syncLibrariesHint')"
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
v-model="mediaServerInfo.config.scan_mode"
:label="t('mediaserver.scanMode')"
:items="ugreenScanModeOptions"
:hint="t('mediaserver.scanModeHint')"
persistent-hint
active
prepend-inner-icon="mdi-radar"
/>
</VCol>
</VRow>
<VRow v-else-if="mediaServerInfo.type == 'plex'"> <VRow v-else-if="mediaServerInfo.type == 'plex'">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField

View File

@@ -188,6 +188,7 @@ export function useSetupWizard() {
'jellyfin': 'JellyfinModule', 'jellyfin': 'JellyfinModule',
'plex': 'PlexModule', 'plex': 'PlexModule',
'trimemedia': 'TrimeMediaModule', 'trimemedia': 'TrimeMediaModule',
'ugreen': 'UgreenModule',
}, },
// 通知映射 // 通知映射
notification: { notification: {
@@ -405,7 +406,7 @@ export function useSetupWizard() {
errors.push(t('mediaserver.tokenRequired')) errors.push(t('mediaserver.tokenRequired'))
validationErrors.value.mediaServer.token = true validationErrors.value.mediaServer.token = true
} }
} else if (wizardData.value.mediaServer.type === 'trimemedia') { } else if (wizardData.value.mediaServer.type === 'trimemedia' || wizardData.value.mediaServer.type === 'ugreen') {
if (!wizardData.value.mediaServer.config?.username?.trim()) { if (!wizardData.value.mediaServer.config?.username?.trim()) {
errors.push(t('mediaserver.usernameRequired')) errors.push(t('mediaserver.usernameRequired'))
validationErrors.value.mediaServer.username = true validationErrors.value.mediaServer.username = true

View File

@@ -46,6 +46,7 @@ export default {
unsubscribe: 'Unsubscribe', unsubscribe: 'Unsubscribe',
media: 'Media', media: 'Media',
unknown: 'Unknown', unknown: 'Unknown',
notFetched: 'Not Fetched',
notice: 'Notice', notice: 'Notice',
itemsPerPage: 'Items per page', itemsPerPage: 'Items per page',
pageText: '{0}-{1} of {2}', pageText: '{0}-{1} of {2}',
@@ -314,7 +315,8 @@ export default {
settingTabs: { settingTabs: {
system: { system: {
title: 'System', title: 'System',
description: 'Basic settings, downloaders (Qbittorrent, Transmission), media servers (Emby, Jellyfin, Plex)', description:
'Basic settings, downloaders (Qbittorrent, Transmission), media servers (Emby, Jellyfin, Plex, TrimeMedia, Ugreen)',
}, },
directory: { directory: {
title: 'Storage & Directories', title: 'Storage & Directories',
@@ -1341,6 +1343,7 @@ export default {
emby: 'Emby', emby: 'Emby',
jellyfin: 'Jellyfin', jellyfin: 'Jellyfin',
plex: 'Plex', plex: 'Plex',
ugreen: 'Ugreen',
reloadSuccess: 'System configuration has taken effect', reloadSuccess: 'System configuration has taken effect',
reloadFailed: 'Failed to reload system!', reloadFailed: 'Failed to reload system!',
auxAuthEnable: 'User Auxiliary Authentication', auxAuthEnable: 'User Auxiliary Authentication',
@@ -2853,6 +2856,13 @@ export default {
password: 'Password', password: 'Password',
syncLibraries: 'Sync Libraries', syncLibraries: 'Sync Libraries',
syncLibrariesHint: 'Only selected libraries will be synchronized', syncLibrariesHint: 'Only selected libraries will be synchronized',
scanMode: 'Scan Mode',
scanModeHint: 'Applies to full-library and targeted refresh: New & Modified / Supplement Missing / Full Override',
scanModeOptions: {
newAndModified: 'New & Modified',
supplementMissing: 'Supplement Missing',
fullOverride: 'Full Override',
},
hostRequired: 'Host cannot be empty', hostRequired: 'Host cannot be empty',
apiKeyRequired: 'API Key cannot be empty', apiKeyRequired: 'API Key cannot be empty',
tokenRequired: 'Token cannot be empty', tokenRequired: 'Token cannot be empty',
@@ -3160,7 +3170,8 @@ export default {
title: 'Media Server', title: 'Media Server',
description: 'Configure media server', description: 'Configure media server',
info: 'Media Server Configuration', info: 'Media Server Configuration',
infoDesc: 'Configure media server for media library management, can choose Emby, Jellyfin or Plex etc.', infoDesc:
'Configure media server for media library management, can choose Emby, Jellyfin, Plex, TrimeMedia or Ugreen.',
type: 'Media Server Type', type: 'Media Server Type',
typeHint: 'Select the type of media server to use', typeHint: 'Select the type of media server to use',
name: 'Server Name', name: 'Server Name',

View File

@@ -46,6 +46,7 @@ export default {
unsubscribe: '取消订阅', unsubscribe: '取消订阅',
media: '媒体', media: '媒体',
unknown: '未知', unknown: '未知',
notFetched: '未获取',
notice: '注意', notice: '注意',
itemsPerPage: '每页条数', itemsPerPage: '每页条数',
pageText: '{0}-{1} 共 {2} 条', pageText: '{0}-{1} 共 {2} 条',
@@ -313,7 +314,7 @@ export default {
settingTabs: { settingTabs: {
system: { system: {
title: '系统', title: '系统',
description: '基础设置、下载器Qbittorrent、Transmission、媒体服务器Emby、Jellyfin、Plex', description: '基础设置、下载器Qbittorrent、Transmission、媒体服务器Emby、Jellyfin、Plex、飞牛影视、绿联影视',
}, },
directory: { directory: {
title: '存储 & 目录', title: '存储 & 目录',
@@ -1336,6 +1337,7 @@ export default {
emby: 'Emby', emby: 'Emby',
jellyfin: 'Jellyfin', jellyfin: 'Jellyfin',
plex: 'Plex', plex: 'Plex',
ugreen: '绿联影视',
reloadSuccess: '系统配置已生效', reloadSuccess: '系统配置已生效',
reloadFailed: '重载系统失败!', reloadFailed: '重载系统失败!',
auxAuthEnable: '用户辅助认证', auxAuthEnable: '用户辅助认证',
@@ -2817,6 +2819,13 @@ export default {
password: '密码', password: '密码',
syncLibraries: '同步媒体库', syncLibraries: '同步媒体库',
syncLibrariesHint: '只有选中的媒体库才会被同步', syncLibrariesHint: '只有选中的媒体库才会被同步',
scanMode: '扫描模式',
scanModeHint: '用于全库刷新和按库刷新:新添加和修改 / 补充缺失 / 覆盖扫描',
scanModeOptions: {
newAndModified: '新添加和修改',
supplementMissing: '补充缺失',
fullOverride: '覆盖扫描',
},
nameExists: '【{name}】已存在,请替换为其他名称', nameExists: '【{name}】已存在,请替换为其他名称',
hostRequired: '地址不能为空', hostRequired: '地址不能为空',
apiKeyRequired: 'API密钥不能为空', apiKeyRequired: 'API密钥不能为空',
@@ -3123,7 +3132,7 @@ export default {
title: '媒体服务器', title: '媒体服务器',
description: '配置媒体服务器', description: '配置媒体服务器',
info: '媒体服务器配置说明', info: '媒体服务器配置说明',
infoDesc: '配置媒体服务器用于媒体库管理可选择Emby、JellyfinPlex', infoDesc: '配置媒体服务器用于媒体库管理可选择Emby、JellyfinPlex、飞牛影视或绿联影视',
type: '媒体服务器类型', type: '媒体服务器类型',
typeHint: '选择要使用的媒体服务器类型', typeHint: '选择要使用的媒体服务器类型',
name: '服务器名称', name: '服务器名称',

View File

@@ -46,6 +46,7 @@ export default {
unsubscribe: '取消訂閱', unsubscribe: '取消訂閱',
media: '媒體', media: '媒體',
unknown: '未知', unknown: '未知',
notFetched: '未獲取',
notice: '注意', notice: '注意',
itemsPerPage: '每頁條數', itemsPerPage: '每頁條數',
pageText: '{0}-{1} 共 {2} 條', pageText: '{0}-{1} 共 {2} 條',
@@ -313,7 +314,7 @@ export default {
settingTabs: { settingTabs: {
system: { system: {
title: '系統', title: '系統',
description: '基礎設置、下載器Qbittorrent、Transmission、媒體服務器Emby、Jellyfin、Plex', description: '基礎設置、下載器Qbittorrent、Transmission、媒體服務器Emby、Jellyfin、Plex、飛牛影視、綠聯影視',
}, },
directory: { directory: {
title: '存儲 & 目錄', title: '存儲 & 目錄',
@@ -1337,6 +1338,7 @@ export default {
emby: 'Emby', emby: 'Emby',
jellyfin: 'Jellyfin', jellyfin: 'Jellyfin',
plex: 'Plex', plex: 'Plex',
ugreen: '綠聯影視',
reloadSuccess: '系統配置已生效', reloadSuccess: '系統配置已生效',
reloadFailed: '重載系統失敗!', reloadFailed: '重載系統失敗!',
auxAuthEnable: '用戶輔助認證', auxAuthEnable: '用戶輔助認證',
@@ -2823,6 +2825,13 @@ export default {
password: '密碼', password: '密碼',
syncLibraries: '同步媒體庫', syncLibraries: '同步媒體庫',
syncLibrariesHint: '只有選中的媒體庫才會被同步', syncLibrariesHint: '只有選中的媒體庫才會被同步',
scanMode: '掃描模式',
scanModeHint: '用於全庫刷新和按庫刷新:新添加和修改 / 補充缺失 / 覆蓋掃描',
scanModeOptions: {
newAndModified: '新添加和修改',
supplementMissing: '補充缺失',
fullOverride: '覆蓋掃描',
},
nameExists: '【{name}】已存在,請替換為其他名稱', nameExists: '【{name}】已存在,請替換為其他名稱',
}, },
bangumi: { bangumi: {
@@ -3124,7 +3133,7 @@ export default {
title: '媒體伺服器', title: '媒體伺服器',
description: '設定媒體伺服器', description: '設定媒體伺服器',
info: '媒體伺服器設定說明', info: '媒體伺服器設定說明',
infoDesc: '設定媒體伺服器用於媒體庫管理可選擇Emby、JellyfinPlex', infoDesc: '設定媒體伺服器用於媒體庫管理可選擇Emby、JellyfinPlex、飛牛影視或綠聯影視',
type: '媒體伺服器類型', type: '媒體伺服器類型',
typeHint: '選擇要使用的媒體伺服器類型', typeHint: '選擇要使用的媒體伺服器類型',
name: '伺服器名稱', name: '伺服器名稱',

View File

@@ -11,6 +11,7 @@ import embyLogo from '@/assets/images/logos/emby.png'
import jellyfinLogo from '@/assets/images/logos/jellyfin.png' import jellyfinLogo from '@/assets/images/logos/jellyfin.png'
import plexLogo from '@/assets/images/logos/plex.png' import plexLogo from '@/assets/images/logos/plex.png'
import trimemediaLogo from '@/assets/images/logos/trimemedia.png' import trimemediaLogo from '@/assets/images/logos/trimemedia.png'
import ugreenLogo from '@/assets/images/logos/ugreen.jpg'
import wechatLogo from '@/assets/images/logos/wechat.png' import wechatLogo from '@/assets/images/logos/wechat.png'
import telegramLogo from '@/assets/images/logos/telegram.webp' import telegramLogo from '@/assets/images/logos/telegram.webp'
import slackLogo from '@/assets/images/logos/slack.webp' import slackLogo from '@/assets/images/logos/slack.webp'
@@ -40,6 +41,7 @@ const logoMap: Record<string, string> = {
jellyfin: jellyfinLogo, jellyfin: jellyfinLogo,
plex: plexLogo, plex: plexLogo,
trimemedia: trimemediaLogo, trimemedia: trimemediaLogo,
ugreen: ugreenLogo,
wechat: wechatLogo, wechat: wechatLogo,
telegram: telegramLogo, telegram: telegramLogo,
slack: slackLogo, slack: slackLogo,

View File

@@ -28,7 +28,7 @@ async function loadMediaStatistic() {
}, },
{ {
title: t('dashboard.episodes'), title: t('dashboard.episodes'),
stats: res.episode_count.toLocaleString(), stats: res.episode_count == null ? t('common.notFetched') : res.episode_count.toLocaleString(),
icon: 'mdi-television-classic', icon: 'mdi-television-classic',
color: 'warning', color: 'warning',
}, },

View File

@@ -15,6 +15,20 @@ const librariesOptions = ref<{ title: string; value: string | undefined }[]>([
}, },
]) ])
const ugreenScanModeOptions = computed(() => [
{ title: t('mediaserver.scanModeOptions.newAndModified'), value: 'new_and_modified' },
{ title: t('mediaserver.scanModeOptions.supplementMissing'), value: 'supplement_missing' },
{ title: t('mediaserver.scanModeOptions.fullOverride'), value: 'full_override' },
])
function ensureUgreenScanMode() {
if (wizardData.value.mediaServer.type !== 'ugreen') return
wizardData.value.mediaServer.config = wizardData.value.mediaServer.config || {}
if (!wizardData.value.mediaServer.config.scan_mode) {
wizardData.value.mediaServer.config.scan_mode = 'supplement_missing'
}
}
// 调用API查询媒体库 // 调用API查询媒体库
async function loadLibrary(server: string) { async function loadLibrary(server: string) {
try { try {
@@ -42,6 +56,7 @@ async function loadLibrary(server: string) {
// 选择媒体服务器并自动加载媒体库 // 选择媒体服务器并自动加载媒体库
async function selectMediaServerWithLibrary(type: string) { async function selectMediaServerWithLibrary(type: string) {
selectMediaServer(type) selectMediaServer(type)
ensureUgreenScanMode()
// 如果选择了媒体服务器类型,自动加载媒体库 // 如果选择了媒体服务器类型,自动加载媒体库
if (type && wizardData.value.mediaServer.name) { if (type && wizardData.value.mediaServer.name) {
await loadLibrary(wizardData.value.mediaServer.name) await loadLibrary(wizardData.value.mediaServer.name)
@@ -50,6 +65,7 @@ async function selectMediaServerWithLibrary(type: string) {
// 组件挂载时检查是否需要加载媒体库 // 组件挂载时检查是否需要加载媒体库
onMounted(async () => { onMounted(async () => {
ensureUgreenScanMode()
// 如果已经有媒体服务器配置,自动加载媒体库 // 如果已经有媒体服务器配置,自动加载媒体库
if (wizardData.value.mediaServer.type && wizardData.value.mediaServer.name) { if (wizardData.value.mediaServer.type && wizardData.value.mediaServer.name) {
await loadLibrary(wizardData.value.mediaServer.name) await loadLibrary(wizardData.value.mediaServer.name)
@@ -60,6 +76,7 @@ onMounted(async () => {
watch( watch(
() => [wizardData.value.mediaServer.type, wizardData.value.mediaServer.name], () => [wizardData.value.mediaServer.type, wizardData.value.mediaServer.name],
async ([type, name]) => { async ([type, name]) => {
ensureUgreenScanMode()
console.log('Media server changed:', { type, name }) console.log('Media server changed:', { type, name })
if (type && name) { if (type && name) {
await loadLibrary(name) await loadLibrary(name)
@@ -141,6 +158,19 @@ watch(
</VCardText> </VCardText>
</VCard> </VCard>
</VCol> </VCol>
<VCol cols="12" md="3">
<VCard
:color="wizardData.mediaServer.type === 'ugreen' ? 'primary' : 'default'"
:variant="wizardData.mediaServer.type === 'ugreen' ? 'tonal' : 'outlined'"
class="cursor-pointer"
@click="selectMediaServerWithLibrary('ugreen')"
>
<VCardText class="text-center">
<VImg :src="getLogoUrl('ugreen')" height="48" width="48" class="mx-auto mb-2" />
<div class="text-h6">绿联影视</div>
</VCardText>
</VCard>
</VCol>
</VRow> </VRow>
</div> </div>
</VCol> </VCol>
@@ -380,6 +410,97 @@ watch(
/> />
</VCol> </VCol>
</VRow> </VRow>
<VRow v-else-if="wizardData.mediaServer.type === 'ugreen'">
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.mediaServer.name"
:label="t('common.name')"
:placeholder="t('mediaserver.nameRequired')"
:hint="t('mediaserver.serverAlias')"
:error="validationErrors.mediaServer.name"
:error-messages="validationErrors.mediaServer.name ? [t('mediaserver.nameRequired')] : []"
persistent-hint
active
prepend-inner-icon="mdi-label"
required
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.mediaServer.config.host"
:label="t('mediaserver.host')"
:placeholder="t('mediaserver.hostPlaceholder')"
:hint="t('mediaserver.hostHint')"
:error="validationErrors.mediaServer.host"
:error-messages="validationErrors.mediaServer.host ? [t('mediaserver.hostRequired')] : []"
persistent-hint
active
prepend-inner-icon="mdi-server"
required
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="wizardData.mediaServer.config.play_host"
:label="t('mediaserver.playHost')"
:placeholder="t('mediaserver.playHostPlaceholder')"
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.mediaServer.config.username"
:label="t('mediaserver.username')"
:error="validationErrors.mediaServer.username"
:error-messages="validationErrors.mediaServer.username ? [t('mediaserver.usernameRequired')] : []"
active
prepend-inner-icon="mdi-account"
required
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
type="password"
v-model="wizardData.mediaServer.config.password"
:label="t('mediaserver.password')"
:error="validationErrors.mediaServer.password"
:error-messages="validationErrors.mediaServer.password ? [t('mediaserver.passwordRequired')] : []"
active
prepend-inner-icon="mdi-lock"
required
/>
</VCol>
<VCol cols="12">
<VAutocomplete
v-model="wizardData.mediaServer.sync_libraries"
:label="t('mediaserver.syncLibraries')"
:items="librariesOptions"
chips
multiple
clearable
:hint="t('mediaserver.syncLibrariesHint')"
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(wizardData.mediaServer.name)"
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
v-model="wizardData.mediaServer.config.scan_mode"
:label="t('mediaserver.scanMode')"
:items="ugreenScanModeOptions"
:hint="t('mediaserver.scanModeHint')"
persistent-hint
active
prepend-inner-icon="mdi-radar"
/>
</VCol>
</VRow>
<VRow v-else-if="wizardData.mediaServer.type === 'plex'"> <VRow v-else-if="wizardData.mediaServer.type === 'plex'">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField

1003
yarn.lock

File diff suppressed because it is too large Load Diff