Compare commits

..

70 Commits

Author SHA1 Message Date
jxxghp
9c893abcdf fix: 优化登录页面密码管理器自动填充 2026-03-30 12:56:17 +08:00
jxxghp
ead891ca2f 更新 AccountSettingSystem.vue 2026-03-27 22:22:17 +08:00
jxxghp
8713e3cc86 feat: Add AI agent verbose mode, rename scheduled wake setting to scheduled wake, and update system settings layout. 2026-03-27 20:59:53 +08:00
jxxghp
3cc83d10d3 refactor: Relocate scheduler service settings from the main settings page to a new dedicated system view accessible via the shortcut bar. 2026-03-25 13:37:59 +08:00
jxxghp
192ded374a feat:增加 AI_AGENT_JOB_INTERVAL 设置项 2026-03-25 13:06:44 +08:00
jxxghp
13997c7e74 Merge pull request #457 from wikrin/style/settings-ui 2026-03-20 21:30:54 +08:00
jxxghp
71b0dd4cc2 更新 package.json 2026-03-19 22:25:59 +08:00
Attente
a58a0cdffe refactor(AccountSettingSystem): 重构按钮图标结构样式 2026-03-19 21:47:52 +08:00
jxxghp
6aeb040db4 Merge pull request #456 from wikrin/refactor/scraping-switch-to-policy 2026-03-19 21:35:15 +08:00
Attente
fef20e361e refactor(setting): 更新刮削策略设置界面 2026-03-19 20:13:16 +08:00
jxxghp
a63a07701d 更新 package.json 2026-03-14 18:03:02 +08:00
jxxghp
5dd56f2db3 Merge pull request #455 from EkkoG/wechat_bot 2026-03-14 18:02:18 +08:00
EkkoG
275b095574 feat(wechat): implement AI bot mode configuration and localization updates
- Added functionality to enable AI bot mode for WeChat notifications, including default configuration settings.
- Introduced new input fields for bot-specific settings such as Bot ID, Bot Secret, and WebSocket URL.
- Updated localization files for English, Simplified Chinese, and Traditional Chinese to include new bot-related labels and hints.
2026-03-14 16:18:02 +08:00
jxxghp
05eae71fba Merge pull request #454 from YuF-9468/fix-issue-438-episode-zero 2026-03-13 22:39:25 +08:00
YuF-9468
777b3c9445 refactor(media): remove any cast for episode count fields 2026-03-13 15:31:24 +08:00
YuF-9468
a214168b1e fix(media): avoid false in-library badge for TV seasons with zero episodes 2026-03-13 14:38:58 +08:00
jxxghp
9d55d02557 Merge pull request #453 from DDSRem-Dev/v2 2026-03-11 15:28:30 +08:00
DDSRem
16c084ba80 fix(plugin): build remote entry URL with origin+pathname to fix subpath proxy 404
- Use pathBase (pathname) when building remoteEntry URL so it matches API request base
- Fixes plugin static assets 404 when app is under subpath (e.g. /mp/)

Made-with: Cursor
2026-03-11 15:12:42 +08:00
jxxghp
b0f4ccc186 Merge pull request #451 from WongWang/feat-plugin-priority 2026-03-10 12:55:00 +08:00
jxxghp
96d0606b4d chore: bump version to 2.9.14 2026-03-08 08:52:53 +08:00
jxxghp
450b9ec28a feat: Add QQ logo for qqbot notifications, update Ugreen logo to PNG, and adjust VImg styling in media server card. 2026-03-08 08:52:29 +08:00
jxxghp
2ccf03fc1b Merge pull request #452 from EkkoG/qqbot 2026-03-08 07:51:02 +08:00
EkkoG
38dfb3af07 feat: add QQ notification channel support with validation and localization 2026-03-07 23:21:09 +08:00
Castell
ae4c59bfdb feat: 新增优先使用插件识别的功能 2026-03-02 21:04:43 +08:00
jxxghp
c9f4fdbee8 Merge pull request #450 from baozaodetudou/v2 2026-03-01 08:08:35 +08:00
doumao
d21f461dda Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2026-02-28 22:58:29 +08:00
doumao
28a5a83315 feat: 前端支持绿联SSL证书校验开关配置 2026-02-28 22:54:27 +08:00
jxxghp
11d11b88bf Merge pull request #449 from baozaodetudou/v2 2026-02-28 22:45:12 +08:00
doumao
ff7658b5ba feat: 完成绿联媒体服务前端接入与展示优化 2026-02-28 22:09:09 +08:00
jxxghp
351faf2891 更新 package.json 2026-02-28 12:52:34 +08:00
jxxghp
7d66229bad Merge pull request #448 from wumode/fix-progress-displaying 2026-02-28 12:34:17 +08:00
wumode
2b08be1e7d fix(reorganize): add progress tracking for log transfer 2026-02-28 01:17:18 +08:00
wumode
8255cfd479 fix(reorganize): dynamically update progress SSE connection based on item path 2026-02-27 16:20:04 +08:00
jxxghp
f356bb4407 更新 package.json 2026-02-22 16:11:33 +08:00
jxxghp
07e60291a2 Merge pull request #446 from DDSRem-Dev/rtorrent 2026-02-22 16:11:12 +08:00
DDSRem
2dbe8e6685 feat(downloader): add rTorrent UI support
Add rTorrent as a downloader option in settings, setup wizard, and
downloader card with config form (host, username, password) and
ruTorrent logo. Include i18n translations for zh-CN, zh-TW, and en-US.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 13:12:56 +08:00
jxxghp
40f36b2afd Merge pull request #445 from cddjr/feat/media_card_list_deduplication 2026-02-17 07:15:17 +08:00
景大侠
d4260d5103 fix(types): 移除由提交 1dab013 引入的 episode_group 重复定义 2026-02-13 22:10:28 +08:00
景大侠
45f68bc936 feat(MediaCardListView): 添加去重逻辑以防止重复项 2026-02-13 19:08:20 +08:00
jxxghp
9469074837 v2.9.11 2026-02-12 07:01:38 +08:00
jxxghp
193807bb6f Merge pull request #444 from cddjr/fix_s0 2026-02-06 12:48:57 +08:00
景大侠
d4548db5b9 fix: 完善几处S00订阅相关的问题 2026-02-06 12:48:32 +08:00
jxxghp
29aaea6fe6 更新 package.json 2026-02-06 12:22:59 +08:00
jxxghp
369cc6438f Merge pull request #443 from jxxghp/copilot/fix-season-null-value 2026-02-06 12:22:30 +08:00
copilot-swe-agent[bot]
d80b39c77b Add package-lock.json to .gitignore and remove it from repo
Co-authored-by: jxxghp <51039935+jxxghp@users.noreply.github.com>
2026-02-06 04:07:50 +00:00
copilot-swe-agent[bot]
626725a8ca Fix: pass null instead of 0 for season parameter when subscribing to movies
Co-authored-by: jxxghp <51039935+jxxghp@users.noreply.github.com>
2026-02-06 04:07:13 +00:00
copilot-swe-agent[bot]
8be96358ae Initial plan 2026-02-06 04:01:58 +00:00
jxxghp
f2bfbfa3c5 更新 package.json 2026-02-02 12:36:27 +08:00
jxxghp
7c9ffd6abc Merge pull request #442 from CHANTXU64/v2 2026-02-02 12:36:11 +08:00
CHANTXU64
b370354287 fix: TMDB 剧集详情页支持显示第 0 季(特别篇)并将其排序到末尾 (jxxghp/MoviePilot#5444) 2026-02-02 10:20:40 +08:00
jxxghp
145d71e283 Merge pull request #441 from cddjr/feat_group_select 2026-01-31 13:28:40 +08:00
景大侠
eeea82d815 feat(TransferHistory): 增加分组选择功能 2026-01-31 11:29:03 +08:00
jxxghp
babd267bc4 更新 package.json 2026-01-29 22:22:42 +08:00
jxxghp
e136c931ac Merge pull request #440 from DDSRem-Dev/v2 2026-01-29 22:22:03 +08:00
DDSRem
ae00602345 feat: u115 support oauth 2026-01-29 21:49:17 +08:00
jxxghp
5382108ee7 Merge pull request #439 from DemoJameson/fix-nickname-2 2026-01-29 11:58:39 +08:00
DemoJameson
514063d3fb fix: 删除昵称后保存无效 2026-01-29 11:31:51 +08:00
jxxghp
b08f396fec Merge pull request #437 from DemoJameson/fix-nickname 2026-01-26 18:42:10 +08:00
DemoJameson
d37a7f06f1 fix: 个人信息页面不显示昵称 2026-01-26 13:34:22 +08:00
jxxghp
ad7bca3aae feat: 更新了分类配置的API端点,并为对话框添加了小屏幕全屏显示功能。 2026-01-26 12:52:46 +08:00
jxxghp
4fb70ba80e 更新 package.json 2026-01-25 15:57:11 +08:00
jxxghp
1225b2eb9e Merge pull request #435 from z-henry/v2
修复:按照指定剧集组订阅,日历读取不到对应的日期
2026-01-25 15:50:01 +08:00
jxxghp
24b2f103b9 Merge branch 'v2' into v2 2026-01-25 15:49:15 +08:00
jxxghp
0d304b58ca feat:二级分类设置界面 2026-01-25 09:40:50 +08:00
HenryZZZZZ
f419dbd794 Merge branch 'jxxghp:v2' into v2 2026-01-24 16:04:45 +08:00
jxxghp
7854cc81a8 fix message ui 2026-01-24 11:52:49 +08:00
jxxghp
9ad1bd29bd fix markdown ui 2026-01-23 22:46:25 +08:00
jxxghp
b88d4f0ecb feat:LLM上下文窗口设置 2026-01-23 22:35:03 +08:00
HenryZZZZZ
44168b62d2 Merge pull request #1 from z-henry/codex/update-api-call-for-episode-group
Add episode_group query param for TMDB season episode requests
2026-01-23 11:09:50 +08:00
HenryZZZZZ
1dab013436 Add episode_group param to TMDB episode requests 2026-01-23 11:08:55 +08:00
42 changed files with 2322 additions and 527 deletions

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ dist
dist-ssr dist-ssr
dev-dist dev-dist
*.local *.local
package-lock.json
/cypress/videos/ /cypress/videos/
/cypress/screenshots/ /cypress/screenshots/

View File

@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "2.9.5", "version": "2.9.23",
"private": true, "private": true,
"type": "module", "type": "module",
"bin": "dist/service.js", "bin": "dist/service.js",
@@ -127,4 +127,4 @@
"workbox-window": "^7.3.0" "workbox-window": "^7.3.0"
}, },
"packageManager": "yarn@1.22.18" "packageManager": "yarn@1.22.18"
} }

View File

@@ -52,6 +52,10 @@ export const downloaderOptions = [
value: 'transmission', value: 'transmission',
title: i18n.global.t('setting.system.transmission'), title: i18n.global.t('setting.system.transmission'),
}, },
{
value: 'rtorrent',
title: i18n.global.t('setting.system.rtorrent'),
},
] ]
export const downloaderDict = downloaderOptions.reduce((dict, item) => { export const downloaderDict = downloaderOptions.reduce((dict, item) => {
@@ -76,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 }
@@ -1445,4 +1445,19 @@ export interface ApiResponse<T = any> {
success: boolean success: boolean
message?: string message?: string
data: T data: T
} }
// 分类规则
export interface CategoryRule {
genre_ids?: string
original_language?: string
production_countries?: string
origin_country?: string
release_year?: string
}
// 分类配置
export interface CategoryConfig {
movie?: { [key: string]: CategoryRule }
tv?: { [key: string]: CategoryRule }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 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

@@ -206,6 +206,8 @@ const getIcon = computed(() => {
return getLogoUrl('qbittorrent') return getLogoUrl('qbittorrent')
case 'transmission': case 'transmission':
return getLogoUrl('transmission') return getLogoUrl('transmission')
case 'rtorrent':
return getLogoUrl('rtorrent')
default: default:
return getLogoUrl('downloader') return getLogoUrl('downloader')
} }
@@ -443,6 +445,51 @@ onUnmounted(() => {
/> />
</VCol> </VCol>
</VRow> </VRow>
<VRow v-else-if="downloaderInfo.type == 'rtorrent'">
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.name"
:label="t('downloader.name')"
:placeholder="t('downloader.nameRequired')"
:hint="t('downloader.name')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.config.host"
:label="t('downloader.host')"
placeholder="http(s)://ip:port/RPC2"
:hint="t('downloader.rtorrentHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.config.username"
:label="t('downloader.username')"
:hint="t('downloader.username')"
persistent-hint
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.config.password"
type="password"
:label="t('downloader.password')"
:hint="t('downloader.password')"
persistent-hint
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
</VRow>
<VRow v-else> <VRow v-else>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField

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

@@ -18,9 +18,14 @@ import { hasPermission } from '@/utils/permission'
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
interface MediaCardMedia extends MediaInfo {
total_episode?: number
episode_count?: number
}
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
media: Object as PropType<MediaInfo>, media: Object as PropType<MediaCardMedia>,
width: String, width: String,
height: String, height: String,
}) })
@@ -138,7 +143,7 @@ async function handleAddSubscribe() {
} }
// 调用API添加订阅电视剧的话需要指定季 // 调用API添加订阅电视剧的话需要指定季
async function addSubscribe(season: number = 0, best_version: number = 0) { async function addSubscribe(season: number | null = null, best_version: number = 0) {
// 开始处理 // 开始处理
startNProgress() startNProgress()
try { try {
@@ -153,7 +158,7 @@ async function addSubscribe(season: number = 0, best_version: number = 0) {
doubanid: props.media?.douban_id, doubanid: props.media?.douban_id,
bangumiid: props.media?.bangumi_id, bangumiid: props.media?.bangumi_id,
mediaid: props.media?.media_id ? `${props.media?.mediaid_prefix}:${props.media?.media_id}` : '', mediaid: props.media?.media_id ? `${props.media?.mediaid_prefix}:${props.media?.media_id}` : '',
season, season: props.media?.type === '电影' ? null : season,
best_version, best_version,
episode_group: episodeGroup.value, episode_group: episodeGroup.value,
}) })
@@ -183,8 +188,8 @@ async function addSubscribe(season: number = 0, best_version: number = 0) {
} }
// 弹出添加订阅提示 // 弹出添加订阅提示
function showSubscribeAddToast(result: boolean, title: string, season: number, message: string, best_version: number) { function showSubscribeAddToast(result: boolean, title: string, season: number | null, message: string, best_version: number) {
if (season) title = `${title} ${formatSeason(season.toString())}` if (season !== null) title = `${title} ${formatSeason(season.toString())}`
let subname = t('subscribe.normalSub') let subname = t('subscribe.normalSub')
if (best_version > 0) subname = t('subscribe.versionSub') if (best_version > 0) subname = t('subscribe.versionSub')
@@ -222,7 +227,7 @@ async function removeSubscribe() {
// 查询当前媒体是否已订阅 // 查询当前媒体是否已订阅
async function handleCheckSubscribe() { async function handleCheckSubscribe() {
try { try {
const result = await checkSubscribe(props.media?.season) const result = await checkSubscribe(props.media?.season ?? null)
if (result) isSubscribed.value = true if (result) isSubscribed.value = true
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -232,6 +237,14 @@ async function handleCheckSubscribe() {
// 查询当前媒体是否已入库 // 查询当前媒体是否已入库
async function handleCheckExists() { async function handleCheckExists() {
try { try {
// 对于总集数为 0 的电视剧季TMDB 未返回有效集数),不展示“已入库”角标,避免误判
const totalEpisode = props.media?.total_episode ?? props.media?.episode_count ?? props.media?.number_of_episodes ?? 0
if (props.media?.type === '电视剧' && totalEpisode === 0) {
isExists.value = false
return
}
const result: { [key: string]: any } = await api.get('mediaserver/exists', { const result: { [key: string]: any } = await api.get('mediaserver/exists', {
params: { params: {
tmdbid: props.media?.tmdb_id, tmdbid: props.media?.tmdb_id,
@@ -249,7 +262,7 @@ async function handleCheckExists() {
} }
// 调用API检查是否已订阅电视剧需要指定季 // 调用API检查是否已订阅电视剧需要指定季
async function checkSubscribe(season = 0) { async function checkSubscribe(season: number | null) {
try { try {
// AbortController 现在由全局请求优化器自动管理 // AbortController 现在由全局请求优化器自动管理
const mediaid = getMediaId() const mediaid = getMediaId()
@@ -300,7 +313,7 @@ function subscribeSeasons(seasons: MediaSeason[], seasonNoExists: { [key: number
if (season && props.media?.tmdb_id) if (season && props.media?.tmdb_id)
// 全部存在时洗版 // 全部存在时洗版
best_version = !seasonNoExists[season.season_number || 0] ? 1 : 0 best_version = !seasonNoExists[season.season_number || 0] ? 1 : 0
addSubscribe(season.season_number, best_version) addSubscribe(season.season_number ?? null, best_version)
}) })
} }

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,15 @@ 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.config.scan_mode) {
mediaServerInfo.value.config.scan_mode = 'supplement_missing'
}
if (mediaServerInfo.value.config.verify_ssl === undefined) {
mediaServerInfo.value.config.verify_ssl = true
}
}
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 +125,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:
@@ -196,7 +213,7 @@ onMounted(() => {
<span class="me-2 mb-1">自定义媒体服务器</span> <span class="me-2 mb-1">自定义媒体服务器</span>
</div> </div>
</div> </div>
<VImg :src="getIcon" cover class="mt-8 me-3" max-width="3rem" min-width="3rem" /> <VImg :src="getIcon" class="mt-8 me-3 max-h-12" max-width="3rem" min-width="3rem" />
</VCardText> </VCardText>
</VCard> </VCard>
@@ -424,6 +441,95 @@ 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>
<VCol cols="12" md="6">
<VSwitch
v-model="mediaServerInfo.config.verify_ssl"
:label="t('mediaserver.verifySsl')"
:hint="t('mediaserver.verifySslHint')"
persistent-hint
color="primary"
inset
/>
</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

@@ -102,7 +102,7 @@ function renderMarkdown(value: string) {
</VCardTitle> </VCardTitle>
<div <div
v-if="props.message?.text && props.message?.action === 0" v-if="props.message?.text && props.message?.action === 0"
class="rounded-md text-body-1 py-1 px-4 elevation-2 bg-primary text-white chat-right mb-1" class="rounded-md text-body-1 py-1 px-4 elevation-2 bg-primary text-white chat-right"
> >
<div class="markdown-body" v-html="renderMarkdown(props.message?.text)" /> <div class="markdown-body" v-html="renderMarkdown(props.message?.text)" />
</div> </div>
@@ -155,12 +155,23 @@ function renderMarkdown(value: string) {
text-decoration: underline; text-decoration: underline;
} }
ul, ul {
ol { list-style-type: disc;
margin-block-end: 0.5rem; margin-block-end: 0.5rem;
padding-inline-start: 1.5rem; padding-inline-start: 1.5rem;
} }
ol {
list-style-type: decimal;
margin-block-end: 0.5rem;
padding-inline-start: 1.5rem;
}
li {
display: list-item;
margin-block-end: 0.25rem;
}
code { code {
border-radius: 4px; border-radius: 4px;
background-color: rgba(var(--v-border-color), 0.1); background-color: rgba(var(--v-border-color), 0.1);

View File

@@ -46,6 +46,7 @@ const notificationInfo = ref<NotificationConf>({
const notificationTypeNames: { [key: string]: string } = { const notificationTypeNames: { [key: string]: string } = {
wechat: t('notification.wechat.name'), wechat: t('notification.wechat.name'),
telegram: t('notification.telegram.name'), telegram: t('notification.telegram.name'),
qqbot: t('notification.qqbot.name'),
vocechat: t('notification.vocechat.name'), vocechat: t('notification.vocechat.name'),
synologychat: t('notification.synologychat.name'), synologychat: t('notification.synologychat.name'),
slack: t('notification.slack.name'), slack: t('notification.slack.name'),
@@ -66,10 +67,39 @@ const notificationTypes = [
{ value: '其它', title: t('notificationSwitch.other') }, { value: '其它', title: t('notificationSwitch.other') },
] ]
function ensureWechatConfigDefaults(notification: NotificationConf) {
if (notification.type !== 'wechat') {
return
}
if (!notification.config) {
notification.config = {}
}
if (!notification.config.WECHAT_MODE) {
notification.config.WECHAT_MODE = 'app'
}
if (!notification.config.WECHAT_BOT_WS_URL) {
notification.config.WECHAT_BOT_WS_URL = 'wss://openws.work.weixin.qq.com'
}
}
const isWechatBotMode = computed({
get: () => notificationInfo.value.config?.WECHAT_MODE === 'bot',
set: value => {
if (!notificationInfo.value.config) {
notificationInfo.value.config = {}
}
notificationInfo.value.config.WECHAT_MODE = value ? 'bot' : 'app'
if (value && !notificationInfo.value.config.WECHAT_BOT_WS_URL) {
notificationInfo.value.config.WECHAT_BOT_WS_URL = 'wss://openws.work.weixin.qq.com'
}
},
})
// 打开详情弹窗 // 打开详情弹窗
function openNotificationInfoDialog() { function openNotificationInfoDialog() {
// 替换成深复制,避免修改时影响原数据 // 替换成深复制,避免修改时影响原数据
notificationInfo.value = cloneDeep(props.notification) notificationInfo.value = cloneDeep(props.notification)
ensureWechatConfigDefaults(notificationInfo.value)
notificationInfoDialog.value = true notificationInfoDialog.value = true
} }
@@ -85,6 +115,7 @@ function saveNotificationInfo() {
$toast.error(t('notification.channel') + `${notificationInfo.value.name}` + t('common.exists')) $toast.error(t('notification.channel') + `${notificationInfo.value.name}` + t('common.exists'))
return return
} }
ensureWechatConfigDefaults(notificationInfo.value)
notificationInfoDialog.value = false notificationInfoDialog.value = false
emit('change', notificationInfo.value, props.notification.name) emit('change', notificationInfo.value, props.notification.name)
emit('done') emit('done')
@@ -97,6 +128,8 @@ const getIcon = computed(() => {
return getLogoUrl('wechat') return getLogoUrl('wechat')
case 'telegram': case 'telegram':
return getLogoUrl('telegram') return getLogoUrl('telegram')
case 'qqbot':
return getLogoUrl('qq')
case 'vocechat': case 'vocechat':
return getLogoUrl('vocechat') return getLogoUrl('vocechat')
case 'synologychat': case 'synologychat':
@@ -187,69 +220,129 @@ function onClose() {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VSwitch
v-model="notificationInfo.config.WECHAT_CORPID" v-model="isWechatBotMode"
:label="t('notification.wechat.corpId')" :label="t('notification.wechat.useBotMode')"
:hint="t('notification.wechat.corpIdHint')" :hint="t('notification.wechat.useBotModeHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-domain" color="primary"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_APP_ID"
:label="t('notification.wechat.appId')"
:hint="t('notification.wechat.appIdHint')"
persistent-hint
prepend-inner-icon="mdi-application"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_APP_SECRET"
:label="t('notification.wechat.appSecret')"
:hint="t('notification.wechat.appSecretHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_PROXY"
:label="t('notification.wechat.proxy')"
:hint="t('notification.wechat.proxyHint')"
persistent-hint
prepend-inner-icon="mdi-server-network"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_TOKEN"
:label="t('notification.wechat.token')"
:hint="t('notification.wechat.tokenHint')"
persistent-hint
prepend-inner-icon="mdi-key-variant"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_ENCODING_AESKEY"
:label="t('notification.wechat.encodingAesKey')"
:hint="t('notification.wechat.encodingAesKeyHint')"
persistent-hint
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_ADMINS"
:label="t('notification.wechat.admins')"
:placeholder="t('notification.wechat.adminsPlaceholder')"
:hint="t('notification.wechat.adminsHint')"
persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/> />
</VCol> </VCol>
<template v-if="isWechatBotMode">
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_BOT_ID"
:label="t('notification.wechat.botId')"
:hint="t('notification.wechat.botIdHint')"
persistent-hint
prepend-inner-icon="mdi-robot"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_BOT_SECRET"
:label="t('notification.wechat.botSecret')"
:hint="t('notification.wechat.botSecretHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_BOT_CHAT_ID"
:label="t('notification.wechat.botChatId')"
:placeholder="t('notification.wechat.botChatIdPlaceholder')"
:hint="t('notification.wechat.botChatIdHint')"
persistent-hint
prepend-inner-icon="mdi-chat-processing"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_BOT_WS_URL"
:label="t('notification.wechat.botWsUrl')"
:hint="t('notification.wechat.botWsUrlHint')"
persistent-hint
prepend-inner-icon="mdi-lan-connect"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_ADMINS"
:label="t('notification.wechat.admins')"
:placeholder="t('notification.wechat.adminsPlaceholder')"
:hint="t('notification.wechat.adminsHint')"
persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/>
</VCol>
</template>
<template v-else>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_CORPID"
:label="t('notification.wechat.corpId')"
:hint="t('notification.wechat.corpIdHint')"
persistent-hint
prepend-inner-icon="mdi-domain"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_APP_ID"
:label="t('notification.wechat.appId')"
:hint="t('notification.wechat.appIdHint')"
persistent-hint
prepend-inner-icon="mdi-application"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_APP_SECRET"
:label="t('notification.wechat.appSecret')"
:hint="t('notification.wechat.appSecretHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_PROXY"
:label="t('notification.wechat.proxy')"
:hint="t('notification.wechat.proxyHint')"
persistent-hint
prepend-inner-icon="mdi-server-network"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_TOKEN"
:label="t('notification.wechat.token')"
:hint="t('notification.wechat.tokenHint')"
persistent-hint
prepend-inner-icon="mdi-key-variant"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_ENCODING_AESKEY"
:label="t('notification.wechat.encodingAesKey')"
:hint="t('notification.wechat.encodingAesKeyHint')"
persistent-hint
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHAT_ADMINS"
:label="t('notification.wechat.admins')"
:placeholder="t('notification.wechat.adminsPlaceholder')"
:hint="t('notification.wechat.adminsHint')"
persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/>
</VCol>
</template>
</VRow> </VRow>
<VRow v-else-if="notificationInfo.type == 'telegram'"> <VRow v-else-if="notificationInfo.type == 'telegram'">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -464,6 +557,56 @@ function onClose() {
/> />
</VCol> </VCol>
</VRow> </VRow>
<VRow v-else-if="notificationInfo.type == 'qqbot'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.name"
:label="t('notification.name')"
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.QQ_APP_ID"
:label="t('notification.qqbot.appId')"
:hint="t('notification.qqbot.appIdHint')"
persistent-hint
prepend-inner-icon="mdi-application"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.QQ_APP_SECRET"
:label="t('notification.qqbot.appSecret')"
:hint="t('notification.qqbot.appSecretHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.QQ_OPENID"
:label="t('notification.qqbot.openId')"
:placeholder="t('notification.qqbot.openIdPlaceholder')"
:hint="t('notification.qqbot.openIdHint')"
persistent-hint
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.QQ_GROUP_OPENID"
:label="t('notification.qqbot.groupOpenId')"
:placeholder="t('notification.qqbot.groupOpenIdPlaceholder')"
:hint="t('notification.qqbot.groupOpenIdHint')"
persistent-hint
prepend-inner-icon="mdi-account-group"
/>
</VCol>
</VRow>
<VRow v-else-if="notificationInfo.type == 'webpush'"> <VRow v-else-if="notificationInfo.type == 'webpush'">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField

View File

@@ -0,0 +1,663 @@
<script setup lang="ts">
import draggable from 'vuedraggable'
import api from '@/api'
import type { CategoryConfig } from '@/api/types'
import { useToast } from 'vue-toastification'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 定义输入参数
defineProps<{
modelValue?: boolean
}>()
// 定义事件
const emit = defineEmits(['close', 'save'])
const activeTab = ref('movie')
const loading = ref(false)
const saving = ref(false)
const toast = useToast()
const { t } = useI18n()
const generateId = () => {
return 'id-' + Math.random().toString(36).substr(2, 9) + '-' + Date.now()
}
interface CategoryItem {
id: string
name: string
rule: any
}
const movieList = ref<CategoryItem[]>([])
const tvList = ref<CategoryItem[]>([])
// TMDB 类型映射
const genreOptions = [
{ title: '动作 (Action)', value: '28' },
{ title: '冒险 (Adventure)', value: '12' },
{ title: '动画 (Animation)', value: '16' },
{ title: '喜剧 (Comedy)', value: '35' },
{ title: '犯罪 (Crime)', value: '80' },
{ title: '纪录 (Documentary)', value: '99' },
{ title: '剧情 (Drama)', value: '18' },
{ title: '家庭 (Family)', value: '10751' },
{ title: '奇幻 (Fantasy)', value: '14' },
{ title: '历史 (History)', value: '36' },
{ title: '恐怖 (Horror)', value: '27' },
{ title: '音乐 (Music)', value: '10402' },
{ title: '悬疑 (Mystery)', value: '9648' },
{ title: '爱情 (Romance)', value: '10749' },
{ title: '科幻 (SF)', value: '878' },
{ title: '电视电影', value: '10770' },
{ title: '惊悚 (Thriller)', value: '53' },
{ title: '战争 (War)', value: '10752' },
{ title: '西部 (Western)', value: '37' },
{ title: '儿童 (Kids)', value: '10762' },
{ title: '新闻 (News)', value: '10763' },
{ title: '真人秀 (Reality)', value: '10764' },
{ title: '科幻/奇幻 (Sci-Fi)', value: '10765' },
{ title: '肥皂剧 (Soap)', value: '10766' },
{ title: '访谈 (Talk)', value: '10767' },
{ title: '战争/政治', value: '10768' },
]
// 语种选项 (original_language)
const languageOptions = [
{ title: '中文', value: 'zh' },
{ title: '中文', value: 'cn' },
{ title: '英语 (English)', value: 'en' },
{ title: '日语 (Japanese)', value: 'ja' },
{ title: '韩语 (Korean)', value: 'ko' },
{ title: '法语 (French)', value: 'fr' },
{ title: '德语 (German)', value: 'de' },
{ title: '西班牙语 (Spanish)', value: 'es' },
{ title: '意大利语 (Italian)', value: 'it' },
{ title: '葡萄牙语 (Portuguese)', value: 'pt' },
{ title: '俄语 (Russian)', value: 'ru' },
{ title: '阿拉伯语', value: 'ar' },
{ title: '泰语 (Thai)', value: 'th' },
{ title: '越南语 (Vietnamese)', value: 'vi' },
{ title: '印地语 (Hindi)', value: 'hi' },
{ title: '土耳其语 (Turkish)', value: 'tr' },
{ title: '荷兰语 (Dutch)', value: 'nl' },
{ title: '波兰语 (Polish)', value: 'pl' },
{ title: '瑞典语 (Swedish)', value: 'sv' },
{ title: '丹麦语 (Danish)', value: 'da' },
{ title: '挪威语 (Norwegian)', value: 'nb' },
{ title: '芬兰语 (Finnish)', value: 'fi' },
{ title: '希腊语 (Greek)', value: 'el' },
{ title: '捷克语 (Czech)', value: 'cs' },
{ title: '匈牙利语 (Hungarian)', value: 'hu' },
{ title: '罗马尼亚语 (Romanian)', value: 'ro' },
{ title: '乌克兰语 (Ukrainian)', value: 'uk' },
{ title: '印度尼西亚语 (Indonesian)', value: 'id' },
{ title: '马来语 (Malay)', value: 'ms' },
{ title: '希伯来语 (Hebrew)', value: 'he' },
]
// 国家/地区选项 (origin_country/production_countries)
const countryOptions = [
{ title: '中国大陆 (CN)', value: 'CN' },
{ title: '中国香港 (HK)', value: 'HK' },
{ title: '中国台湾 (TW)', value: 'TW' },
{ title: '美国 (US)', value: 'US' },
{ title: '英国 (GB)', value: 'GB' },
{ title: '日本 (JP)', value: 'JP' },
{ title: '韩国 (KR)', value: 'KR' },
{ title: '法国 (FR)', value: 'FR' },
{ title: '德国 (DE)', value: 'DE' },
{ title: '意大利 (IT)', value: 'IT' },
{ title: '西班牙 (ES)', value: 'ES' },
{ title: '加拿大 (CA)', value: 'CA' },
{ title: '澳大利亚 (AU)', value: 'AU' },
{ title: '俄罗斯 (RU)', value: 'RU' },
{ title: '印度 (IN)', value: 'IN' },
{ title: '泰国 (TH)', value: 'TH' },
{ title: '新加坡 (SG)', value: 'SG' },
{ title: '马来西亚 (MY)', value: 'MY' },
{ title: '越南 (VN)', value: 'VN' },
{ title: '菲律宾 (PH)', value: 'PH' },
{ title: '巴西 (BR)', value: 'BR' },
{ title: '墨西哥 (MX)', value: 'MX' },
{ title: '阿根廷 (AR)', value: 'AR' },
{ title: '荷兰 (NL)', value: 'NL' },
{ title: '比利时 (BE)', value: 'BE' },
{ title: '瑞士 (CH)', value: 'CH' },
{ title: '瑞典 (SE)', value: 'SE' },
{ title: '挪威 (NO)', value: 'NO' },
{ title: '丹麦 (DK)', value: 'DK' },
{ title: '波兰 (PL)', value: 'PL' },
{ title: '捷克 (CZ)', value: 'CZ' },
{ title: '土耳其 (TR)', value: 'TR' },
{ title: '以色列 (IL)', value: 'IL' },
{ title: '埃及 (EG)', value: 'EG' },
{ title: '南非 (ZA)', value: 'ZA' },
{ title: '新西兰 (NZ)', value: 'NZ' },
]
const fetchConfig = async () => {
loading.value = true
try {
const res: any = await api.get('media/category/config')
if (res && res.data) {
parseConfig(res.data)
}
} catch (e) {
console.error(e)
toast.error(t('setting.category.loadFailed'))
} finally {
loading.value = false
}
}
const parseConfig = (data: CategoryConfig) => {
// 将对象 { "Name": { ... } } 转换为数组 [ { id: uuid, name: "Name", rule: { ... } } ]
movieList.value = []
if (data.movie) {
for (const [key, value] of Object.entries(data.movie)) {
// 为了UI一致性处理 genre_ids 为数组或字符串,但 API 发送的是字符串
const rule = { ...value }
if (rule.genre_ids && typeof rule.genre_ids === 'string') {
// UI 多选预期为数组,检查输入。实际上 VAutocomplete 多选预期数组。我们需要将字符串分割为数组。
// @ts-ignore
rule.genre_ids = rule.genre_ids.split(',')
} else {
// @ts-ignore
rule.genre_ids = []
}
// 处理语种
if (rule.original_language && typeof rule.original_language === 'string') {
// @ts-ignore
rule.original_language = rule.original_language.split(',')
} else {
// @ts-ignore
rule.original_language = []
}
// 处理制片国家/地区
if (rule.production_countries && typeof rule.production_countries === 'string') {
// @ts-ignore
rule.production_countries = rule.production_countries.split(',')
} else {
// @ts-ignore
rule.production_countries = []
}
movieList.value.push({
id: generateId(),
name: key,
rule: rule as any,
})
}
}
tvList.value = []
if (data.tv) {
for (const [key, value] of Object.entries(data.tv)) {
const rule = { ...value }
if (rule.genre_ids && typeof rule.genre_ids === 'string') {
// @ts-ignore
rule.genre_ids = rule.genre_ids.split(',')
} else {
// @ts-ignore
rule.genre_ids = []
}
// 处理语种
if (rule.original_language && typeof rule.original_language === 'string') {
// @ts-ignore
rule.original_language = rule.original_language.split(',')
} else {
// @ts-ignore
rule.original_language = []
}
// 处理发行国家/地区
if (rule.origin_country && typeof rule.origin_country === 'string') {
// @ts-ignore
rule.origin_country = rule.origin_country.split(',')
} else {
// @ts-ignore
rule.origin_country = []
}
tvList.value.push({
id: generateId(),
name: key,
rule: rule as any,
})
}
}
}
const addMovieItem = () => {
movieList.value.push({
id: generateId(),
name: '新分类',
rule: { genre_ids: [] as any },
})
}
const removeMovieItem = (index: number) => {
movieList.value.splice(index, 1)
}
const addTvItem = () => {
tvList.value.push({
id: generateId(),
name: '新分类',
rule: { genre_ids: [] as any },
})
}
const removeTvItem = (index: number) => {
tvList.value.splice(index, 1)
}
const saveConfig = async () => {
saving.value = true
try {
// 将数组转换回对象
const payload: CategoryConfig = {
movie: {},
tv: {},
}
movieList.value.forEach(item => {
if (item.name) {
const rule = { ...item.rule }
// 将 genre_ids 数组转换回字符串
if (Array.isArray(rule.genre_ids) && rule.genre_ids.length > 0) {
rule.genre_ids = rule.genre_ids.join(',')
} else {
// @ts-ignore
rule.genre_ids = null
}
// 将 original_language 数组转换回字符串
if (Array.isArray(rule.original_language) && rule.original_language.length > 0) {
rule.original_language = rule.original_language.join(',')
} else {
rule.original_language = undefined
}
// 将 production_countries 数组转换回字符串
if (Array.isArray(rule.production_countries) && rule.production_countries.length > 0) {
rule.production_countries = rule.production_countries.join(',')
} else {
rule.production_countries = undefined
}
// 清理空字符串
if (!rule.release_year) rule.release_year = undefined
// @ts-ignore
payload.movie[item.name] = rule
}
})
tvList.value.forEach(item => {
if (item.name) {
const rule = { ...item.rule }
if (Array.isArray(rule.genre_ids) && rule.genre_ids.length > 0) {
rule.genre_ids = rule.genre_ids.join(',')
} else {
// @ts-ignore
rule.genre_ids = null
}
// 将 original_language 数组转换回字符串
if (Array.isArray(rule.original_language) && rule.original_language.length > 0) {
rule.original_language = rule.original_language.join(',')
} else {
rule.original_language = undefined
}
// 将 origin_country 数组转换回字符串
if (Array.isArray(rule.origin_country) && rule.origin_country.length > 0) {
rule.origin_country = rule.origin_country.join(',')
} else {
rule.origin_country = undefined
}
// 清理空字符串
if (!rule.release_year) rule.release_year = undefined
// @ts-ignore
payload.tv[item.name] = rule
}
})
const res: any = await api.post('media/category/config', payload)
if (res && res.success) {
toast.success(t('setting.category.saveSuccess'))
emit('save')
emit('close')
} else {
toast.error(t('setting.category.saveFailed', { message: res.message || 'Error' }))
}
} catch (e) {
console.error(e)
toast.error(t('setting.category.saveFailed', { message: 'Network or Config Error' }))
} finally {
saving.value = false
}
}
onMounted(() => {
fetchConfig()
})
</script>
<template>
<VDialog :model-value="modelValue" max-width="1000" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardItem class="py-3">
<template #prepend>
<VIcon icon="mdi-shape-outline" class="me-2" />
</template>
<VCardTitle>
{{ t('setting.category.title') }}
</VCardTitle>
<VCardSubtitle>
{{ t('setting.category.subtitle') }}
</VCardSubtitle>
</VCardItem>
<VCardText>
<VTabs v-model="activeTab" show-arrows class="mb-4">
<VTab value="movie">
<VIcon icon="mdi-movie-outline" class="me-2" />
{{ t('setting.category.movie') }}
</VTab>
<VTab value="tv">
<VIcon icon="mdi-television" class="me-2" />
{{ t('setting.category.tv') }}
</VTab>
</VTabs>
<div v-if="loading" class="d-flex justify-center align-center" style="min-height: 300px">
<VProgressCircular indeterminate color="primary" size="64" />
</div>
<VWindow v-else v-model="activeTab" class="disable-tab-transition" :touch="false">
<VWindowItem value="movie">
<draggable v-model="movieList" handle=".drag-handle" item-key="id" animation="200">
<template #item="{ element, index }">
<VCard variant="tonal" class="mb-4 category-item">
<VCardText class="pa-4">
<div class="d-flex align-center mb-5">
<VTextField
v-model="element.name"
:label="t('setting.category.name')"
density="comfortable"
hide-details
variant="plain"
class="font-bold"
prepend-inner-icon="mdi-tag-outline"
/>
<VSpacer />
<VBtn
icon="mdi-drag-vertical"
variant="text"
size="small"
class="drag-handle me-2"
color="primary"
/>
<VBtn
icon="mdi-delete-outline"
color="error"
variant="text"
size="small"
@click="removeMovieItem(index)"
/>
</div>
<VRow>
<VCol cols="12" md="6">
<VAutocomplete
v-model="element.rule.genre_ids"
:items="genreOptions"
:label="t('setting.category.genre')"
item-title="title"
item-value="value"
multiple
chips
closable-chips
density="comfortable"
variant="outlined"
persistent-hint
prepend-inner-icon="mdi-movie-filter-outline"
/>
</VCol>
<VCol cols="12" md="6">
<VAutocomplete
v-model="element.rule.production_countries"
:items="countryOptions"
:label="t('setting.category.country')"
item-title="title"
item-value="value"
multiple
chips
closable-chips
density="comfortable"
variant="outlined"
persistent-hint
prepend-inner-icon="mdi-earth"
/>
</VCol>
<VCol cols="12" md="6">
<VAutocomplete
v-model="element.rule.original_language"
:items="languageOptions"
:label="t('setting.category.language')"
item-title="title"
item-value="value"
multiple
chips
closable-chips
density="comfortable"
variant="outlined"
persistent-hint
prepend-inner-icon="mdi-translate"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="element.rule.release_year"
:label="t('setting.category.year')"
:placeholder="t('setting.category.yearPlaceholder')"
density="comfortable"
variant="outlined"
persistent-hint
prepend-inner-icon="mdi-calendar-range"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
</template>
</draggable>
<VBtn
block
variant="outlined"
size="large"
prepend-icon="mdi-plus-circle-outline"
class="mt-2 add-category-btn"
@click="addMovieItem"
>
{{ t('setting.category.addMovie') }}
</VBtn>
</VWindowItem>
<VWindowItem value="tv">
<draggable v-model="tvList" handle=".drag-handle" item-key="id" animation="200">
<template #item="{ element, index }">
<VCard variant="tonal" class="mb-4 category-item">
<VCardText class="pa-4">
<div class="d-flex align-center mb-5">
<VTextField
v-model="element.name"
:label="t('setting.category.name')"
density="comfortable"
hide-details
variant="plain"
class="font-bold"
prepend-inner-icon="mdi-tag-outline"
/>
<VSpacer />
<VBtn
icon="mdi-drag-vertical"
variant="text"
size="small"
class="drag-handle me-2"
color="primary"
/>
<VBtn
icon="mdi-delete-outline"
color="error"
variant="text"
size="small"
@click="removeTvItem(index)"
/>
</div>
<VRow>
<VCol cols="12" md="6">
<VAutocomplete
v-model="element.rule.genre_ids"
:items="genreOptions"
:label="t('setting.category.genre')"
item-title="title"
item-value="value"
multiple
chips
closable-chips
density="comfortable"
variant="outlined"
persistent-hint
prepend-inner-icon="mdi-movie-filter-outline"
/>
</VCol>
<VCol cols="12" md="6">
<VAutocomplete
v-model="element.rule.origin_country"
:items="countryOptions"
:label="t('setting.category.country')"
item-title="title"
item-value="value"
multiple
chips
closable-chips
density="comfortable"
variant="outlined"
persistent-hint
prepend-inner-icon="mdi-earth"
/>
</VCol>
<VCol cols="12" md="6">
<VAutocomplete
v-model="element.rule.original_language"
:items="languageOptions"
:label="t('setting.category.language')"
item-title="title"
item-value="value"
multiple
chips
closable-chips
density="comfortable"
variant="outlined"
persistent-hint
prepend-inner-icon="mdi-translate"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="element.rule.release_year"
:label="t('setting.category.year')"
:placeholder="t('setting.category.yearPlaceholder')"
density="comfortable"
variant="outlined"
persistent-hint
prepend-inner-icon="mdi-calendar-range"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
</template>
</draggable>
<VBtn
block
variant="outlined"
size="large"
prepend-icon="mdi-plus-circle-outline"
class="mt-2 add-category-btn"
@click="addTvItem"
>
{{ t('setting.category.addTv') }}
</VBtn>
</VWindowItem>
</VWindow>
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn variant="text" @click="emit('close')">
{{ t('common.cancel') }}
</VBtn>
<VBtn color="primary" :loading="saving" prepend-icon="mdi-content-save" class="px-5" @click="saveConfig">
{{ t('common.save') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>
<style scoped>
.drag-handle {
cursor: grab;
opacity: 0.6;
transition: opacity 0.2s ease;
}
.drag-handle:hover {
opacity: 1;
}
.drag-handle:active {
cursor: grabbing;
}
.category-item {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
}
.category-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.add-category-btn {
border-style: dashed !important;
transition: all 0.2s ease;
}
.add-category-btn:hover {
border-style: solid !important;
transform: translateY(-1px);
}
.disable-tab-transition > * {
transition: none !important;
}
</style>

View File

@@ -10,6 +10,7 @@ import { FileItem, StorageConf, TransferDirectoryConf, TransferForm } from '@/ap
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useGlobalSettingsStore } from '@/stores' import { useGlobalSettingsStore } from '@/stores'
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization' import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
import CryptoJS from 'crypto-js'
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
@@ -63,6 +64,9 @@ const progressText = ref(t('dialog.reorganize.processing'))
// 整理进度 // 整理进度
const progressValue = ref(0) const progressValue = ref(0)
// 进度SSE连接
const progressSSE = ref<any>(null)
// 所有存储 // 所有存储
const storages = ref<StorageConf[]>([]) const storages = ref<StorageConf[]>([])
@@ -200,25 +204,31 @@ function handleProgressMessage(event: MessageEvent) {
} }
} }
// 使用优化的进度SSE连接
const progressSSE = useProgressSSE(
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`,
handleProgressMessage,
'reorganize-progress',
progressActive,
)
// 使用SSE监听加载进度 // 使用SSE监听加载进度
function startLoadingProgress() { function startLoadingProgress(key: string) {
progressText.value = t('dialog.reorganize.processing') progressText.value = t('dialog.reorganize.processing')
progressActive.value = true progressActive.value = true
progressSSE.start()
// 如果已经有连接,先停止
if (progressSSE.value) {
progressSSE.value.stop()
}
const url = `${import.meta.env.VITE_API_BASE_URL}system/progress/${key}`
// 创建新的SSE连接
progressSSE.value = useProgressSSE(url, handleProgressMessage, `reorganize-progress-${key}`, progressActive)
progressSSE.value.start()
} }
// 停止监听加载进度 // 停止监听加载进度
function stopLoadingProgress() { function stopLoadingProgress() {
progressActive.value = false progressActive.value = false
progressSSE.stop() if (progressSSE.value) {
progressSSE.value.stop()
progressSSE.value = null
}
} }
// 整理文件 // 整理文件
@@ -228,25 +238,30 @@ async function transfer(background: boolean = false) {
// 显示进度条 // 显示进度条
progressDialog.value = true progressDialog.value = true
if (!background) {
// 开始监听进度
startLoadingProgress()
}
// 文件整理 // 文件整理
if (props.items) { if (props.items) {
for (const item of props.items) { for (const item of props.items) {
if (!background) {
// 如果是文件计算MD5
const key = item.type === 'dir' ? 'filetransfer' : CryptoJS.MD5(item.path).toString()
// 开始监听进度
startLoadingProgress(key)
}
await handleTransfer(item, background) await handleTransfer(item, background)
} }
} }
// 日志整理 // 日志整理
if (props.logids) { if (props.logids) {
if (!background) {
// 为日志整理任务开启进度监听
startLoadingProgress('filetransfer')
}
for (const logid of props.logids) { for (const logid of props.logids) {
await handleTransferLog(logid, background) await handleTransferLog(logid, background)
} }
} }
if (!background) { if (!background) {
// 停止监听进度 // 停止监听进度
stopLoadingProgress() stopLoadingProgress()

View File

@@ -1,16 +1,22 @@
<script lang="ts" setup> <script lang="ts" setup>
import api from '@/api' import api from '@/api'
import QRCode from 'qrcode'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
// 常量定义
const AUTH_WINDOW_WIDTH = 600
const AUTH_WINDOW_HEIGHT = 700
const POLL_INTERVAL = 2000
const AUTH_STATUS_SUCCESS = 2
const AUTH_STATUS_FAILED = -1
// 显示器宽度 // 显示器宽度
const display = useDisplay() const display = useDisplay()
// 多语言支持 // 多语言支持
const { t } = useI18n() const { t } = useI18n()
// 定义输入 // Props 定义
const props = defineProps({ const props = defineProps({
conf: { conf: {
type: Object as PropType<{ [key: string]: any }>, type: Object as PropType<{ [key: string]: any }>,
@@ -18,27 +24,40 @@ const props = defineProps({
}, },
}) })
// 定义事件 // Events 定义
const emit = defineEmits(['done', 'close']) const emit = defineEmits(['done', 'close'])
// 二维码内容 // 响应式状态
const qrCodeContent = ref('') const authUrl = ref('')
const authState = ref('')
const text = ref('')
const alertType = ref<'success' | 'info' | 'error' | 'warning'>('info')
// 二维码图片 base64 // 授权窗口引用
const qrCodeImage = ref('') let authWindow: Window | null = null
let pollTimer: NodeJS.Timeout | undefined
// 下方的提示信息 // 清理资源
const text = ref(t('dialog.u115Auth.scanQrCode')) function cleanup() {
if (pollTimer) {
clearTimeout(pollTimer)
pollTimer = undefined
}
if (authWindow && !authWindow.closed) {
authWindow.close()
authWindow = null
}
}
// 提醒类型 // 设置提示消息
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info') function setMessage(type: typeof alertType.value, message: string) {
alertType.value = type
text.value = message
}
// timeout定时器 // 完成授权
let timeoutTimer: NodeJS.Timeout | undefined = undefined function handleDone() {
cleanup()
// 完成
async function handleDone() {
clearTimeout(timeoutTimer)
emit('done') emit('done')
} }
@@ -47,78 +66,118 @@ async function handleReset() {
try { try {
const result: { [key: string]: any } = await api.get('/storage/reset/u115') const result: { [key: string]: any } = await api.get('/storage/reset/u115')
if (result.success) { if (result.success) {
// 重置成功 setMessage('success', t('dialog.u115Auth.authSuccess'))
alertType.value = 'success'
handleDone() handleDone()
} else {
alertType.value = 'error'
text.value = result.message
} }
} catch (e) { else {
console.error(e) setMessage('error', result.message || t('dialog.u115Auth.authFailed'))
}
} }
} catch (error) {
// 调用/u115/qrcode api生成二维码 console.error('Reset failed:', error)
async function getQrcode() { setMessage('error', t('dialog.u115Auth.authFailed'))
try {
const result: { [key: string]: any } = await api.get('/storage/qrcode/u115')
if (result.success && result.data) {
qrCodeContent.value = result.data.codeContent
// 生成二维码图片
qrCodeImage.value = await QRCode.toDataURL(result.data.codeContent, {
width: 200,
margin: 1,
})
timeoutTimer = setTimeout(checkQrcode, 3000)
} else {
text.value = result.message
}
} catch (e) {
console.error(e)
} }
} }
// 调用/aliyun/check api验证二维码 // 获取授权URL
async function checkQrcode() { async function fetchAuthUrl() {
try {
const result: { [key: string]: any } = await api.get('/storage/auth_url/u115')
if (result.success && result.data) {
authUrl.value = result.data.authUrl
authState.value = result.data.state
}
else {
setMessage('error', result.message || t('dialog.u115Auth.urlFetchFailed'))
}
}
catch (error) {
console.error('Fetch auth URL failed:', error)
setMessage('error', t('dialog.u115Auth.urlFetchFailed'))
}
}
// 打开授权窗口
function openAuthWindow() {
if (!authUrl.value) {
setMessage('error', t('dialog.u115Auth.urlEmpty'))
return
}
const left = (window.screen.width - AUTH_WINDOW_WIDTH) / 2
const top = (window.screen.height - AUTH_WINDOW_HEIGHT) / 2
const features = [
`width=${AUTH_WINDOW_WIDTH}`,
`height=${AUTH_WINDOW_HEIGHT}`,
`left=${left}`,
`top=${top}`,
'toolbar=no',
'location=no',
'status=no',
'menubar=no',
'scrollbars=yes',
'resizable=yes',
].join(',')
authWindow = window.open(authUrl.value, '115授权', features)
if (authWindow) {
setMessage('info', t('dialog.u115Auth.authorizing'))
pollTimer = setTimeout(checkAuthStatus, POLL_INTERVAL)
}
else {
setMessage('error', t('dialog.u115Auth.popupBlocked'))
}
}
// 检查授权状态
async function checkAuthStatus() {
try { try {
const result: { [key: string]: any } = await api.get('/storage/check/u115') const result: { [key: string]: any } = await api.get('/storage/check/u115')
if (result.success && result.data) { if (result.success && result.data) {
const status = result.data.status const { status, tip } = result.data
text.value = result.data.tip
if (status == 0) { if (status === AUTH_STATUS_SUCCESS) {
alertType.value = 'info' // 授权成功
// 新建、待扫码 setMessage('success', t('dialog.u115Auth.authSuccess'))
clearTimeout(timeoutTimer)
timeoutTimer = setTimeout(checkQrcode, 3000)
} else if (status == 1) {
// 已扫码
alertType.value = 'info'
text.value = t('dialog.u115Auth.scanned')
clearTimeout(timeoutTimer)
timeoutTimer = setTimeout(checkQrcode, 3000)
} else if (status == 2) {
// 已确认完成
alertType.value = 'success'
handleDone() handleDone()
} else { return
// 过期或者已取消
alertType.value = 'error'
} }
} else {
alertType.value = 'error' if (status === AUTH_STATUS_FAILED) {
text.value = result.message // 授权失败或过期
setMessage('error', tip || t('dialog.u115Auth.authFailed'))
cleanup()
return
}
// status === 0 或 1继续等待
} }
} catch (e) {
console.error(e)
} }
catch (error) {
console.error('Check auth status failed:', error)
}
// 检查窗口是否被用户关闭
if (authWindow?.closed) {
setMessage('warning', t('dialog.u115Auth.authCanceled'))
cleanup()
return
}
// 继续轮询
pollTimer = setTimeout(checkAuthStatus, POLL_INTERVAL)
} }
onMounted(async () => { // 生命周期钩子
await getQrcode() onMounted(() => {
fetchAuthUrl()
}) })
onUnmounted(() => { onUnmounted(() => {
if (timeoutTimer) clearTimeout(timeoutTimer) cleanup()
}) })
</script> </script>
@@ -126,37 +185,63 @@ onUnmounted(() => {
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value"> <VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard> <VCard>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VCardItem> <VCardItem>
<template #prepend> <template #prepend>
<VIcon icon="mdi-qrcode" class="me-2" /> <VIcon icon="mdi-shield-key" class="me-2" />
</template> </template>
<VCardTitle> <VCardTitle>
{{ t('dialog.u115Auth.loginTitle') }} {{ t('dialog.u115Auth.loginTitle') }}
</VCardTitle> </VCardTitle>
</VCardItem> </VCardItem>
<VDivider /> <VDivider />
<VCardText class="pt-2 flex flex-col items-center justify-center"> <VCardText class="pt-2 flex flex-col items-center justify-center">
<div class="mt-6 rounded text-center p-3 border"> <!-- 授权按钮 -->
<VImg class="mx-auto" :src="qrCodeImage" width="200" height="200"> <div class="mt-6 mb-4 text-center">
<template #placeholder> <VBtn
<div class="w-full h-full"> size="x-large"
<VSkeletonLoader class="object-cover aspect-w-1 aspect-h-1" /> color="primary"
</div> prepend-icon="mdi-login"
</template> :disabled="!authUrl"
</VImg> class="px-8"
@click="openAuthWindow"
>
{{ t('dialog.u115Auth.openAuthWindow') }}
</VBtn>
</div> </div>
<div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text"> <!-- 状态提示 -->
<div v-if="text" class="w-full">
<VAlert
variant="tonal"
:type="alertType"
:text="text"
class="my-4 text-center"
>
<template #prepend /> <template #prepend />
</VAlert> </VAlert>
</div> </div>
</VCardText> </VCardText>
<VCardActions> <VCardActions>
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3"> <VBtn
color="error"
prepend-icon="mdi-restore"
class="px-5 me-3"
@click="handleReset"
>
{{ t('dialog.u115Auth.reset') }} {{ t('dialog.u115Auth.reset') }}
</VBtn> </VBtn>
<VSpacer /> <VSpacer />
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
<VBtn
prepend-icon="mdi-check"
class="px-5 me-3"
@click="handleDone"
>
{{ t('dialog.u115Auth.complete') }} {{ t('dialog.u115Auth.complete') }}
</VBtn> </VBtn>
</VCardActions> </VCardActions>

View File

@@ -199,6 +199,7 @@ async function fetchUserInfo() {
userForm.value = await api.get(`user/${props.username}`) userForm.value = await api.get(`user/${props.username}`)
if (userForm.value) { if (userForm.value) {
userForm.value.avatar = userForm.value.avatar || avatar1 userForm.value.avatar = userForm.value.avatar || avatar1
userForm.value.nickname = userForm.value.settings?.nickname ?? ''
currentAvatar.value = userForm.value.avatar currentAvatar.value = userForm.value.avatar
currentUserName.value = userForm.value.name currentUserName.value = userForm.value.name
userName.value = userForm.value.name userName.value = userForm.value.name
@@ -273,12 +274,10 @@ async function updateUser() {
} }
// 将nickname保存到settings中后端可以直接处理JSON对象 // 将nickname保存到settings中后端可以直接处理JSON对象
if (userForm.value.nickname) { if (!userForm.value.settings) {
if (!userForm.value.settings) { userForm.value.settings = {}
userForm.value.settings = {}
}
userForm.value.settings.nickname = userForm.value.nickname
} }
userForm.value.settings.nickname = userForm.value.nickname ?? ''
const oldUserName = userForm.value.name const oldUserName = userForm.value.name
userForm.value.name = currentUserName.value userForm.value.name = currentUserName.value

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
@@ -486,6 +487,16 @@ export function useSetupWizard() {
validationErrors.value.notification.VOCECHAT_API_KEY = true validationErrors.value.notification.VOCECHAT_API_KEY = true
} }
break break
case 'qqbot':
if (!config.QQ_APP_ID?.trim()) {
errors.push(t('notification.qqbot.appIdRequired'))
validationErrors.value.notification.QQ_APP_ID = true
}
if (!config.QQ_APP_SECRET?.trim()) {
errors.push(t('notification.qqbot.appSecretRequired'))
validationErrors.value.notification.QQ_APP_SECRET = true
}
break
} }
return { return {

View File

@@ -7,6 +7,7 @@ import ModuleTestView from '@/views/system/ModuleTestView.vue'
import MessageView from '@/views/system/MessageView.vue' import MessageView from '@/views/system/MessageView.vue'
import WordsView from '@/views/system/WordsView.vue' import WordsView from '@/views/system/WordsView.vue'
import CacheView from '@/views/system/CacheView.vue' import CacheView from '@/views/system/CacheView.vue'
import AccountSettingService from '@/views/system/ServiceView.vue'
import api from '@/api' import api from '@/api'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { getQueryValue } from '@/@core/utils' import { getQueryValue } from '@/@core/utils'
@@ -49,6 +50,9 @@ const wordsDialog = ref(false)
// 缓存管理弹窗 // 缓存管理弹窗
const cacheDialog = ref(false) const cacheDialog = ref(false)
// 定时服务弹窗
const schedulerDialog = ref(false)
// 输入消息 // 输入消息
const user_message = ref('') const user_message = ref('')
@@ -108,6 +112,13 @@ const shortcuts = [
dialog: 'cache', dialog: 'cache',
dialogRef: cacheDialog, dialogRef: cacheDialog,
}, },
{
title: t('shortcut.scheduler.title'),
subtitle: t('shortcut.scheduler.subtitle'),
icon: 'mdi-list-box',
dialog: 'scheduler',
dialogRef: schedulerDialog,
},
{ {
title: t('shortcut.system.title'), title: t('shortcut.system.title'),
subtitle: t('shortcut.system.subtitle'), subtitle: t('shortcut.system.subtitle'),
@@ -275,10 +286,10 @@ onMounted(() => {
item.dialog === 'message' item.dialog === 'message'
? openMessageDialog() ? openMessageDialog()
: item.dialog === 'words' : item.dialog === 'words'
? openDialog(item.dialogRef) ? openDialog(item.dialogRef)
: item.dialog === 'cache' : item.dialog === 'cache'
? openDialog(item.dialogRef) ? openDialog(item.dialogRef)
: openDialog(item.dialogRef) : openDialog(item.dialogRef)
" "
> >
<VAvatar variant="text" size="48" rounded="lg"> <VAvatar variant="text" size="48" rounded="lg">
@@ -420,6 +431,29 @@ onMounted(() => {
</VCardText> </VCardText>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 定时服务弹窗 -->
<VDialog
v-if="schedulerDialog"
v-model="schedulerDialog"
max-width="60rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem class="py-2">
<VCardTitle>
<VIcon icon="mdi-list-box" class="me-2" />
{{ t('shortcut.scheduler.subtitle') }}
</VCardTitle>
<VCardSubtitle>{{ t('setting.scheduler.subtitle') }}</VCardSubtitle>
<VDialogCloseBtn @click="schedulerDialog = false" />
</VCardItem>
<VDivider />
<VCardText class="pa-0">
<AccountSettingService />
</VCardText>
</VCard>
</VDialog>
<!-- 系统健康检查弹窗 --> <!-- 系统健康检查弹窗 -->
<VDialog <VDialog
v-if="systemTestDialog" v-if="systemTestDialog"

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',
@@ -433,6 +435,8 @@ export default {
config: 'Configuration', config: 'Configuration',
wechat: { wechat: {
name: 'WeChat Work', name: 'WeChat Work',
useBotMode: 'Use AI Bot',
useBotModeHint: 'Enable WebSocket bot mode with fixed dmPolicy=open and groupPolicy=disabled',
corpId: 'Corp ID', corpId: 'Corp ID',
corpIdHint: 'Corp ID in WeChat Work backend enterprise information', corpIdHint: 'Corp ID in WeChat Work backend enterprise information',
corpIdRequired: 'Corp ID cannot be empty', corpIdRequired: 'Corp ID cannot be empty',
@@ -449,6 +453,15 @@ export default {
tokenHint: 'Token in WeChat Work self-built app -> API message receiving configuration', tokenHint: 'Token in WeChat Work self-built app -> API message receiving configuration',
encodingAesKey: 'EncodingAESKey', encodingAesKey: 'EncodingAESKey',
encodingAesKeyHint: 'EncodingAESKey in WeChat Work self-built app -> API message receiving configuration', encodingAesKeyHint: 'EncodingAESKey in WeChat Work self-built app -> API message receiving configuration',
botId: 'Bot ID',
botIdHint: 'Bot ID of the WeChat Work AI bot',
botSecret: 'Bot Secret',
botSecretHint: 'WebSocket secret of the WeChat Work AI bot',
botChatId: 'Default Target',
botChatIdHint: 'Use user userid; for proactive group messages use group:chatid. Leave empty to notify known interacted users',
botChatIdPlaceholder: 'userid or group:chatid',
botWsUrl: 'WebSocket URL',
botWsUrlHint: 'WebSocket endpoint for the WeChat Work AI bot, usually the default value',
admins: 'Admin Whitelist', admins: 'Admin Whitelist',
adminsHint: 'User IDs that can use admin menu and commands, separated by commas', adminsHint: 'User IDs that can use admin menu and commands, separated by commas',
adminsPlaceholder: 'User IDs list, separated by commas', adminsPlaceholder: 'User IDs list, separated by commas',
@@ -519,6 +532,21 @@ export default {
usernameHint: 'Only push messages to the corresponding logged-in user', usernameHint: 'Only push messages to the corresponding logged-in user',
usernameRequired: 'Username cannot be empty', usernameRequired: 'Username cannot be empty',
}, },
qqbot: {
name: 'QQ',
appId: 'App ID',
appIdHint: 'QQ Open Platform bot App ID',
appIdRequired: 'App ID cannot be empty',
appSecret: 'App Secret',
appSecretHint: 'QQ Open Platform bot App Secret',
appSecretRequired: 'App Secret cannot be empty',
openId: 'User OpenID',
openIdHint: 'Default recipient openid (C2C), user must have interacted with bot before',
openIdPlaceholder: '32-char hex',
groupOpenId: 'Group OpenID',
groupOpenIdHint: 'Default group openid (group chat), use either this or User OpenID',
groupOpenIdPlaceholder: 'Group openid',
},
}, },
shortcut: { shortcut: {
title: 'Shortcuts', title: 'Shortcuts',
@@ -554,6 +582,10 @@ export default {
title: 'Cache', title: 'Cache',
subtitle: 'Manage Cache', subtitle: 'Manage Cache',
}, },
scheduler: {
title: 'Services',
subtitle: 'Scheduled Services',
},
}, },
workflow: { workflow: {
components: 'Action Components', components: 'Action Components',
@@ -1292,23 +1324,42 @@ export default {
llmProviderHint: 'Select the LLM service provider to use', llmProviderHint: 'Select the LLM service provider to use',
llmModel: 'LLM Model Name', llmModel: 'LLM Model Name',
llmModelHint: 'Specify the LLM model to use, such as gpt-3.5-turbo, deepseek-chat, etc.', llmModelHint: 'Specify the LLM model to use, such as gpt-3.5-turbo, deepseek-chat, etc.',
llmMaxContextTokens: 'LLM Max Context Tokens (K)',
llmMaxContextTokensHint:
'Set the maximum number of context tokens (in thousands) for the LLM. Exceeding this limit will trigger context trimming.',
llmApiKey: 'LLM API Key', llmApiKey: 'LLM API Key',
llmApiKeyHint: 'API key from the LLM service provider for authentication', llmApiKeyHint: 'API key from the LLM service provider for authentication',
llmApiKeyPlaceholder: 'Please enter API key', llmApiKeyPlaceholder: 'Please enter API key',
llmBaseUrl: 'LLM Base URL', llmBaseUrl: 'LLM Base URL',
llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints', llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints',
aiAgentGlobal: 'Global AI Assistant', aiAgentGlobal: 'Global AI Assistant',
aiAgentGlobalHint: 'Enable global AI assistant functionality, all message conversations will be answered by the AI agent without using the /ai command', aiAgentGlobalHint:
'Enable global AI assistant functionality, all message conversations will be answered by the AI agent without using the /ai command',
aiAgentJobInterval: 'Scheduled Wake',
aiAgentJobIntervalHint:
'Set the check interval for scheduled wake. Select "Disabled" to disable scheduled tasks.',
aiAgentVerbose: 'Verbose Mode',
aiAgentVerboseHint: 'When enabled, tool call process will be displayed in AI agent responses',
aiAgentJobIntervalDisabled: 'Disabled',
aiAgentJobInterval1h: '1 Hour',
aiAgentJobInterval3h: '3 Hours',
aiAgentJobInterval6h: '6 Hours',
aiAgentJobInterval12h: '12 Hours',
aiAgentJobInterval24h: '24 Hours',
aiAgentJobInterval1w: '1 Week',
aiAgentJobInterval1M: '1 Month',
advancedSettings: 'Advanced Settings', advancedSettings: 'Advanced Settings',
advancedSettingsDesc: 'System advanced settings, only need to be adjusted in special cases', advancedSettingsDesc: 'System advanced settings, only need to be adjusted in special cases',
downloaders: 'Downloaders', downloaders: 'Downloaders',
downloadersDesc: 'Only the default downloader will be used by default.', downloadersDesc: 'Only the default downloader will be used by default.',
aiRecommendEnabled: 'AI Search Recommendation', aiRecommendEnabled: 'AI Search Recommendation',
aiRecommendEnabledHint: 'Enable AI search recommendation. When enabled, an AI recommendation button will be displayed on the search result page, recommending resources based on user preferences.', aiRecommendEnabledHint:
'Enable AI search recommendation. When enabled, an AI recommendation button will be displayed on the search result page, recommending resources based on user preferences.',
aiRecommendUserPreference: 'User Preference', aiRecommendUserPreference: 'User Preference',
aiRecommendUserPreferenceHint: 'Set user preferences for AI recommendation, e.g., 4K WEB-DL Dolby Vision', aiRecommendUserPreferenceHint: 'Set user preferences for AI recommendation, e.g., 4K WEB-DL Dolby Vision',
aiRecommendMaxItems: 'AI Recommendation Analysis Limit', aiRecommendMaxItems: 'AI Recommendation Analysis Limit',
aiRecommendMaxItemsHint: 'Limit the number of search results sent to the AI assistant for analysis. More items mean slower analysis and more token consumption. It is recommended to manually filter to a general range before using AI recommendation.', aiRecommendMaxItemsHint:
'Limit the number of search results sent to the AI assistant for analysis. More items mean slower analysis and more token consumption. It is recommended to manually filter to a general range before using AI recommendation.',
mediaServers: 'Media Servers', mediaServers: 'Media Servers',
mediaServersDesc: 'All enabled media servers will be used.', mediaServersDesc: 'All enabled media servers will be used.',
trimeMedia: 'TrimeMedia', trimeMedia: 'TrimeMedia',
@@ -1331,9 +1382,11 @@ export default {
reloading: 'Applying configuration...', reloading: 'Applying configuration...',
qbittorrent: 'Qbittorrent', qbittorrent: 'Qbittorrent',
transmission: 'Transmission', transmission: 'Transmission',
rtorrent: 'rTorrent',
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',
@@ -1376,6 +1429,8 @@ export default {
fanartEnableHint: 'Use image data from fanart.tv', fanartEnableHint: 'Use image data from fanart.tv',
fanartLang: 'Fanart Language', fanartLang: 'Fanart Language',
fanartLangHint: 'Set language preference for Fanart images, ordered by priority when multiple selected', fanartLangHint: 'Set language preference for Fanart images, ordered by priority when multiple selected',
recognizePluginFirst: "Prioritize Plugin Recognition",
recognizePluginFirstHint: "Prioritize calling plugins for media recognition. If a plugin matches, native recognition will be skipped",
githubProxy: 'Github Acceleration Proxy', githubProxy: 'Github Acceleration Proxy',
githubProxyPlaceholder: 'Leave empty for no proxy', githubProxyPlaceholder: 'Leave empty for no proxy',
githubProxyHint: 'Use proxy to accelerate Github access speed', githubProxyHint: 'Use proxy to accelerate Github access speed',
@@ -1490,6 +1545,11 @@ export default {
episodeThumb: 'Thumb', episodeThumb: 'Thumb',
scrapingSwitchSaveFailed: 'Scraping switch settings save failed: {message}', scrapingSwitchSaveFailed: 'Scraping switch settings save failed: {message}',
scrapingSwitchSaveError: 'Scraping switch settings save failed', scrapingSwitchSaveError: 'Scraping switch settings save failed',
policy: {
skipDesc: 'Skip scraping, this file will not be generated',
missingOnlyDesc: 'Scrape only if missing, existing file remains unchanged',
overwriteDesc: 'Always scrape, existing file will be overwritten',
}
}, },
site: { site: {
siteSync: 'Site Synchronization', siteSync: 'Site Synchronization',
@@ -1587,6 +1647,7 @@ export default {
synologyChat: 'SynologyChat', synologyChat: 'SynologyChat',
voceChat: 'VoceChat', voceChat: 'VoceChat',
webPush: 'WebPush', webPush: 'WebPush',
qq: 'QQ',
custom: 'Custom Notification', custom: 'Custom Notification',
}, },
words: { words: {
@@ -1694,6 +1755,25 @@ export default {
storageSaveSuccess: 'Storage settings saved successfully', storageSaveSuccess: 'Storage settings saved successfully',
storageSaveFailed: 'Failed to save storage settings!', storageSaveFailed: 'Failed to save storage settings!',
}, },
category: {
title: 'Category Policy',
subtitle: 'Configure media auto-categorization rules by type, language, region, etc.',
movie: 'Movies',
tv: 'TV Shows',
name: 'Category Name (Directory)',
genre: 'Genre',
language: 'Language',
languagePlaceholder: 'e.g., en,fr,zh (comma separated)',
country: 'Country/Region',
countryPlaceholder: 'e.g., US,CN,JP',
year: 'Year',
yearPlaceholder: 'e.g., 2023, 2020-2024',
addMovie: 'Add Movie Category',
addTv: 'Add TV Category',
saveSuccess: 'Category policy saved successfully',
loadFailed: 'Failed to load category configuration',
saveFailed: 'Save failed: {message}',
},
rule: { rule: {
customRules: 'Custom Rules', customRules: 'Custom Rules',
customRulesDesc: 'Custom priority rule items', customRulesDesc: 'Custom priority rule items',
@@ -1999,9 +2079,15 @@ export default {
'Before sharing, please ensure the workflow does not contain sensitive information such as PassKey in RSS links to avoid information leakage.', 'Before sharing, please ensure the workflow does not contain sensitive information such as PassKey in RSS links to avoid information leakage.',
}, },
u115Auth: { u115Auth: {
loginTitle: '115 Cloud Login', loginTitle: '115 Cloud Authorization',
scanQrCode: 'Please scan with WeChat or 115 client', openAuthWindow: 'Open Authorization Window',
scanned: 'Scanned, please confirm login', authorizing: 'Please complete authorization in the new window...',
authSuccess: 'Authorization successful!',
authFailed: 'Authorization failed or expired',
authCanceled: 'Authorization canceled, please try again',
urlEmpty: 'Authorization URL is empty',
urlFetchFailed: 'Failed to fetch authorization URL',
popupBlocked: 'Unable to open authorization window, please check browser popup settings',
complete: 'Complete', complete: 'Complete',
reset: 'Reset', reset: 'Reset',
}, },
@@ -2617,7 +2703,8 @@ export default {
passkeyManagement: 'Passkey Management', passkeyManagement: 'Passkey Management',
registerNewPasskey: 'Register New Passkey', registerNewPasskey: 'Register New Passkey',
passkeyDescription: 'Passkeys allow you to sign in quickly and securely without a password.', passkeyDescription: 'Passkeys allow you to sign in quickly and securely without a password.',
passkeyAppDescription: 'Passkeys are a simpler, more secure way to sign in, serving as an alternative to passwords. You can authenticate using passkey-supported apps like iCloud Keychain, Bitwarden, or hardware keys.', passkeyAppDescription:
'Passkeys are a simpler, more secure way to sign in, serving as an alternative to passwords. You can authenticate using passkey-supported apps like iCloud Keychain, Bitwarden, or hardware keys.',
passkeyName: 'Passkey Name', passkeyName: 'Passkey Name',
passkeyNamePlaceholder: 'e.g.: iPhone, Windows Hello', passkeyNamePlaceholder: 'e.g.: iPhone, Windows Hello',
registerPasskey: 'Register Passkey', registerPasskey: 'Register Passkey',
@@ -2631,8 +2718,10 @@ export default {
passkeyDeleteSuccess: 'Passkey deleted', passkeyDeleteSuccess: 'Passkey deleted',
passkeyDeleteFailed: 'Delete failed', passkeyDeleteFailed: 'Delete failed',
deletePasskey: 'Delete Passkey', deletePasskey: 'Delete Passkey',
passkeyDomainWarning: 'The availability of PassKeys is closely related to the {domain}. In a public network environment, please make sure to configure the correct access domain name in "Basic Settings". Domain changes or configuration errors will cause the PassKey to be unusable.', passkeyDomainWarning:
otpRequiredForPasskey: 'For security reasons, you must first enable {otp} before you can register a PassKey. This is to ensure that you can still log in to your account via OTP code if the PassKey becomes invalid due to domain configuration changes.', 'The availability of PassKeys is closely related to the {domain}. In a public network environment, please make sure to configure the correct access domain name in "Basic Settings". Domain changes or configuration errors will cause the PassKey to be unusable.',
otpRequiredForPasskey:
'For security reasons, you must first enable {otp} before you can register a PassKey. This is to ensure that you can still log in to your account via OTP code if the PassKey becomes invalid due to domain configuration changes.',
accessDomain: 'access domain name', accessDomain: 'access domain name',
otpAuthenticator: 'OTP Authenticator', otpAuthenticator: 'OTP Authenticator',
otpGenerateFailed: 'Failed to get OTP URI: {message}!', otpGenerateFailed: 'Failed to get OTP URI: {message}!',
@@ -2641,8 +2730,10 @@ export default {
otpCodeRequired: 'Please enter the 6-digit verification code', otpCodeRequired: 'Please enter the 6-digit verification code',
otpEnableSuccess: 'Two-factor authentication enabled successfully!', otpEnableSuccess: 'Two-factor authentication enabled successfully!',
otpEnableFailed: 'Failed to enable OTP: {message}!', otpEnableFailed: 'Failed to enable OTP: {message}!',
otpDisableRestrictedByPasskey: 'You have registered Passkeys. Please delete all Passkeys before disabling OTP verification.', otpDisableRestrictedByPasskey:
confirmToDisableOtp: 'For security reasons, verifying your login password is required to disable two-factor authentication.', 'You have registered Passkeys. Please delete all Passkeys before disabling OTP verification.',
confirmToDisableOtp:
'For security reasons, verifying your login password is required to disable two-factor authentication.',
confirmToDeletePasskey: 'For security reasons, verifying your login password is required to delete a Passkey.', confirmToDeletePasskey: 'For security reasons, verifying your login password is required to delete a Passkey.',
authenticatorAppDescription: authenticatorAppDescription:
'Use an authenticator app like Google Authenticator, Microsoft Authenticator, Authy, or 1Password to scan the QR code and generate a 6-digit code.', 'Use an authenticator app like Google Authenticator, Microsoft Authenticator, Authy, or 1Password to scan the QR code and generate a 6-digit code.',
@@ -2746,6 +2837,7 @@ export default {
type: 'Type', type: 'Type',
enabled: 'Enabled', enabled: 'Enabled',
customTypeHint: 'Custom downloader type, for plugin scenarios', customTypeHint: 'Custom downloader type, for plugin scenarios',
rtorrentHostHint: 'HTTP: http://ip:port/RPC2 or SCGI: scgi://ip:port',
default: 'Default', default: 'Default',
host: 'Host', host: 'Host',
username: 'Username', username: 'Username',
@@ -2815,6 +2907,15 @@ 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',
verifySsl: 'Verify SSL Certificate',
verifySslHint: 'When enabled, HTTPS certificates are verified; disable for self-signed certificates',
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',
@@ -3122,7 +3223,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: '存储 & 目录',
@@ -432,6 +433,8 @@ export default {
config: '配置', config: '配置',
wechat: { wechat: {
name: '企业微信', name: '企业微信',
useBotMode: '使用智能机器人',
useBotModeHint: '开启后使用智能机器人长连接,固定 dmPolicy=open、groupPolicy=disabled',
corpId: '企业ID', corpId: '企业ID',
corpIdHint: '企业微信后台企业信息中的企业ID', corpIdHint: '企业微信后台企业信息中的企业ID',
corpIdRequired: '企业ID不能为空', corpIdRequired: '企业ID不能为空',
@@ -447,6 +450,15 @@ export default {
tokenHint: '微信企业自建应用->API接收消息配置中的Token', tokenHint: '微信企业自建应用->API接收消息配置中的Token',
encodingAesKey: 'EncodingAESKey', encodingAesKey: 'EncodingAESKey',
encodingAesKeyHint: '微信企业自建应用->API接收消息配置中的EncodingAESKey', encodingAesKeyHint: '微信企业自建应用->API接收消息配置中的EncodingAESKey',
botId: '机器人 BotID',
botIdHint: '企业微信智能机器人的 BotID',
botSecret: '机器人 Secret',
botSecretHint: '企业微信智能机器人长连接专用 Secret',
botChatId: '默认通知目标',
botChatIdHint: '可填写用户 userid如需主动发群消息可填写 group:群聊chatid不填则默认发给已互动用户',
botChatIdPlaceholder: 'userid 或 group:chatid',
botWsUrl: '长连接地址',
botWsUrlHint: '企业微信智能机器人 WebSocket 地址,通常使用默认值',
admins: '管理员白名单', admins: '管理员白名单',
adminsHint: '可使用管理菜单及命令的用户ID列表多个ID使用,分隔', adminsHint: '可使用管理菜单及命令的用户ID列表多个ID使用,分隔',
adminsPlaceholder: '用户ID列表多个ID使用,分隔', adminsPlaceholder: '用户ID列表多个ID使用,分隔',
@@ -517,6 +529,21 @@ export default {
usernameHint: '只有对应的用户登录后才会推送消息', usernameHint: '只有对应的用户登录后才会推送消息',
usernameRequired: '用户名不能为空', usernameRequired: '用户名不能为空',
}, },
qqbot: {
name: 'QQ',
appId: 'AppID',
appIdHint: 'QQ 开放平台机器人 AppID',
appIdRequired: 'AppID 不能为空',
appSecret: 'AppSecret',
appSecretHint: 'QQ 开放平台机器人 AppSecret',
appSecretRequired: 'AppSecret 不能为空',
openId: '用户 OpenID',
openIdHint: '默认接收者 openid单聊用户需曾与机器人交互过',
openIdPlaceholder: '32位十六进制',
groupOpenId: '群组 OpenID',
groupOpenIdHint: '默认群组 openid群聊与用户 OpenID 二选一',
groupOpenIdPlaceholder: '群组 openid',
},
}, },
shortcut: { shortcut: {
title: '捷径', title: '捷径',
@@ -552,6 +579,10 @@ export default {
title: '缓存', title: '缓存',
subtitle: '管理缓存', subtitle: '管理缓存',
}, },
scheduler: {
title: '服务',
subtitle: '定时服务',
},
}, },
workflow: { workflow: {
components: '动作组件', components: '动作组件',
@@ -1288,6 +1319,9 @@ export default {
llmProviderHint: '选择使用的LLM服务提供商', llmProviderHint: '选择使用的LLM服务提供商',
llmModel: 'LLM模型名称', llmModel: 'LLM模型名称',
llmModelHint: '指定使用的LLM模型如gpt-3.5-turbo、deepseek-chat等', llmModelHint: '指定使用的LLM模型如gpt-3.5-turbo、deepseek-chat等',
llmMaxContextTokens: 'LLM 最大上下文 Token 数量 (K)',
llmMaxContextTokensHint:
'设定 LLM 记录会话历史的最大 Token 数量上限(千),超出后将自动修整历史记录以节省 Token 消耗及防止超出 LLM 限制',
llmApiKey: 'LLM API密钥', llmApiKey: 'LLM API密钥',
llmApiKeyHint: 'LLM服务提供商的API密钥用于身份验证', llmApiKeyHint: 'LLM服务提供商的API密钥用于身份验证',
llmApiKeyPlaceholder: '请输入API密钥', llmApiKeyPlaceholder: '请输入API密钥',
@@ -1295,16 +1329,30 @@ export default {
llmBaseUrlHint: 'LLM API的基础URL地址用于自定义API端点', llmBaseUrlHint: 'LLM API的基础URL地址用于自定义API端点',
aiAgentGlobal: '全局智能助手', aiAgentGlobal: '全局智能助手',
aiAgentGlobalHint: '启用全局智能助手功能,所有消息对话均使用智能体回答而不用使用/ai命令', aiAgentGlobalHint: '启用全局智能助手功能,所有消息对话均使用智能体回答而不用使用/ai命令',
aiAgentJobInterval: '定时唤醒',
aiAgentJobIntervalHint: '设置定时唤醒的检查间隔,选择"不启用"则不执行定时任务',
aiAgentVerbose: '啰嗦模式',
aiAgentVerboseHint: '开启后会在智能体回复时显示工具调用过程',
aiAgentJobIntervalDisabled: '不启用',
aiAgentJobInterval1h: '1小时',
aiAgentJobInterval3h: '3小时',
aiAgentJobInterval6h: '6小时',
aiAgentJobInterval12h: '12小时',
aiAgentJobInterval24h: '24小时',
aiAgentJobInterval1w: '1周',
aiAgentJobInterval1M: '1个月',
advancedSettings: '高级设置', advancedSettings: '高级设置',
advancedSettingsDesc: '系统进阶设置,特殊情况下才需要调整', advancedSettingsDesc: '系统进阶设置,特殊情况下才需要调整',
downloaders: '下载器', downloaders: '下载器',
downloadersDesc: '只有默认下载器才会被默认使用。', downloadersDesc: '只有默认下载器才会被默认使用。',
aiRecommendEnabled: '搜索结果智能推荐', aiRecommendEnabled: '搜索结果智能推荐',
aiRecommendEnabledHint: '启用搜索结果智能推荐功能,开启后将在搜索结果页面显示智能推荐按钮,可根据用户偏好智能推荐资源', aiRecommendEnabledHint:
'启用搜索结果智能推荐功能,开启后将在搜索结果页面显示智能推荐按钮,可根据用户偏好智能推荐资源',
aiRecommendUserPreference: '用户偏好', aiRecommendUserPreference: '用户偏好',
aiRecommendUserPreferenceHint: '设置智能推荐时的用户偏好例如4K WEB-DL Dolby Vision', aiRecommendUserPreferenceHint: '设置智能推荐时的用户偏好例如4K WEB-DL Dolby Vision',
aiRecommendMaxItems: '智能推荐分析条目上限', aiRecommendMaxItems: '智能推荐分析条目上限',
aiRecommendMaxItemsHint: '限制发送给智能助手进行分析的搜索结果数量,数量越多分析越慢且消耗 Token 越多,建议先手动筛选,筛选出大致范围后再进行智能推荐', aiRecommendMaxItemsHint:
'限制发送给智能助手进行分析的搜索结果数量,数量越多分析越慢且消耗 Token 越多,建议先手动筛选,筛选出大致范围后再进行智能推荐',
mediaServers: '媒体服务器', mediaServers: '媒体服务器',
mediaServersDesc: '所有启用的媒体服务器都会被使用。', mediaServersDesc: '所有启用的媒体服务器都会被使用。',
trimeMedia: '飞牛影视', trimeMedia: '飞牛影视',
@@ -1327,9 +1375,11 @@ export default {
reloading: '正在应用配置...', reloading: '正在应用配置...',
qbittorrent: 'Qbittorrent', qbittorrent: 'Qbittorrent',
transmission: 'Transmission', transmission: 'Transmission',
rtorrent: 'rTorrent',
emby: 'Emby', emby: 'Emby',
jellyfin: 'Jellyfin', jellyfin: 'Jellyfin',
plex: 'Plex', plex: 'Plex',
ugreen: '绿联影视',
reloadSuccess: '系统配置已生效', reloadSuccess: '系统配置已生效',
reloadFailed: '重载系统失败!', reloadFailed: '重载系统失败!',
auxAuthEnable: '用户辅助认证', auxAuthEnable: '用户辅助认证',
@@ -1369,6 +1419,8 @@ export default {
fanartEnableHint: '使用 fanart.tv 的图片数据', fanartEnableHint: '使用 fanart.tv 的图片数据',
fanartLang: 'Fanart语言', fanartLang: 'Fanart语言',
fanartLangHint: '设置Fanart图片的语言偏好多选时按优先级顺序排列', fanartLangHint: '设置Fanart图片的语言偏好多选时按优先级顺序排列',
recognizePluginFirst: "优先使用插件识别",
recognizePluginFirstHint: "优先调用插件识别媒体信息,若插件命中则不再调用原生识别",
githubProxy: 'Github加速代理', githubProxy: 'Github加速代理',
githubProxyPlaceholder: '留空表示不使用代理', githubProxyPlaceholder: '留空表示不使用代理',
githubProxyHint: '使用代理加速Github访问速度', githubProxyHint: '使用代理加速Github访问速度',
@@ -1479,6 +1531,11 @@ export default {
episodeThumb: '缩略图', episodeThumb: '缩略图',
scrapingSwitchSaveFailed: '刮削开关设置保存失败:{message}', scrapingSwitchSaveFailed: '刮削开关设置保存失败:{message}',
scrapingSwitchSaveError: '刮削开关设置保存失败', scrapingSwitchSaveError: '刮削开关设置保存失败',
policy: {
skipDesc: '跳过刮削,不生成该文件',
missingOnlyDesc: '仅在缺失时刮削,已存在则保持不变',
overwriteDesc: '始终刮削,已存在则覆盖',
}
}, },
site: { site: {
siteSync: '站点同步', siteSync: '站点同步',
@@ -1573,6 +1630,7 @@ export default {
synologyChat: 'SynologyChat', synologyChat: 'SynologyChat',
voceChat: 'VoceChat', voceChat: 'VoceChat',
webPush: 'WebPush', webPush: 'WebPush',
qq: 'QQ',
custom: '自定义通知', custom: '自定义通知',
}, },
words: { words: {
@@ -1672,6 +1730,25 @@ export default {
storageSaveSuccess: '存储设置保存成功', storageSaveSuccess: '存储设置保存成功',
storageSaveFailed: '存储设置保存失败!', storageSaveFailed: '存储设置保存失败!',
}, },
category: {
title: '分类策略',
subtitle: '配置媒体自动分类规则,按类型、语言、地区等条件自动归类',
movie: '电影 (Movie)',
tv: '电视剧 (TV)',
name: '分类名称 (目录名)',
genre: '内容类型 (Genre)',
language: '语种 (Language)',
languagePlaceholder: '如: zh,cn,en (使用逗号分隔)',
country: '国家/地区 (Country)',
countryPlaceholder: '如: US,CN,JP',
year: '年份 (Year)',
yearPlaceholder: '如: 2023, 2020-2024',
addMovie: '添加电影分类',
addTv: '添加电视剧分类',
saveSuccess: '分类策略保存成功',
loadFailed: '加载分类配置失败',
saveFailed: '保存失败: {message}',
},
rule: { rule: {
customRules: '自定义规则', customRules: '自定义规则',
customRulesDesc: '自定义优先级规则项', customRulesDesc: '自定义优先级规则项',
@@ -1971,9 +2048,15 @@ export default {
securityWarningMessage: '分享前请确保工作流没有敏感信息比如RSS链接中的PassKey等避免产生信息泄露。', securityWarningMessage: '分享前请确保工作流没有敏感信息比如RSS链接中的PassKey等避免产生信息泄露。',
}, },
u115Auth: { u115Auth: {
loginTitle: '115网盘登录', loginTitle: '115网盘授权',
scanQrCode: '请使用微信或115客户端扫码', openAuthWindow: '打开授权窗口',
scanned: '已扫码,请确认登录', authorizing: '请在新窗口中完成授权...',
authSuccess: '授权成功!',
authFailed: '授权失败或已过期',
authCanceled: '授权已取消,请重试',
urlEmpty: '授权URL为空',
urlFetchFailed: '获取授权URL失败',
popupBlocked: '无法打开授权窗口,请检查浏览器弹窗设置',
complete: '完成', complete: '完成',
reset: '重置', reset: '重置',
}, },
@@ -2585,7 +2668,8 @@ export default {
passkeyManagement: '通行密钥管理', passkeyManagement: '通行密钥管理',
registerNewPasskey: '注册新通行密钥', registerNewPasskey: '注册新通行密钥',
passkeyDescription: '通行密钥可以让您无需密码即可快速安全地登录。', passkeyDescription: '通行密钥可以让您无需密码即可快速安全地登录。',
passkeyAppDescription: '通行密钥是一种更简单、更安全的登录方式,可以替代密码进行登录。您可以使用 iCloud 钥匙串、Bitwarden 等支持通行密钥的应用程序或硬件密钥完成验证。', passkeyAppDescription:
'通行密钥是一种更简单、更安全的登录方式,可以替代密码进行登录。您可以使用 iCloud 钥匙串、Bitwarden 等支持通行密钥的应用程序或硬件密钥完成验证。',
passkeyName: '通行密钥名称', passkeyName: '通行密钥名称',
passkeyNamePlaceholder: '例如iPhone、Windows Hello', passkeyNamePlaceholder: '例如iPhone、Windows Hello',
registerPasskey: '注册通行密钥', registerPasskey: '注册通行密钥',
@@ -2599,8 +2683,10 @@ export default {
passkeyDeleteSuccess: '通行密钥已删除', passkeyDeleteSuccess: '通行密钥已删除',
passkeyDeleteFailed: '删除失败', passkeyDeleteFailed: '删除失败',
deletePasskey: '删除通行密钥', deletePasskey: '删除通行密钥',
passkeyDomainWarning: '通行密钥PassKey的可用性与 {domain} 紧密相关。在公网环境下,请务必在“基础设置”中配置正确的访问域名。域名变更或配置错误将导致通行密钥无法使用。', passkeyDomainWarning:
otpRequiredForPasskey: '为了安全起见,您必须先启用 {otp} 验证码,然后才能注册通行密钥。这是为了防止在域名配置变动导致 PassKey 失效时,您仍能通过 OTP 码登录账户。', '通行密钥(PassKey)的可用性与 {domain} 紧密相关。在公网环境下,请务必在“基础设置”中配置正确的访问域名。域名变更或配置错误将导致通行密钥无法使用。',
otpRequiredForPasskey:
'为了安全起见,您必须先启用 {otp} 验证码,然后才能注册通行密钥。这是为了防止在域名配置变动导致 PassKey 失效时,您仍能通过 OTP 码登录账户。',
accessDomain: '访问域名', accessDomain: '访问域名',
otpAuthenticator: 'OTP 身份验证器', otpAuthenticator: 'OTP 身份验证器',
otpGenerateFailed: '获取otp uri失败{message}', otpGenerateFailed: '获取otp uri失败{message}',
@@ -2713,6 +2799,7 @@ export default {
type: '类型', type: '类型',
enabled: '启用', enabled: '启用',
customTypeHint: '自定义下载器类型,用于插件等场景', customTypeHint: '自定义下载器类型,用于插件等场景',
rtorrentHostHint: 'HTTP: http://ip:port/RPC2 或 SCGI: scgi://ip:port',
default: '默认', default: '默认',
host: '地址', host: '地址',
username: '用户名', username: '用户名',
@@ -2782,6 +2869,15 @@ export default {
password: '密码', password: '密码',
syncLibraries: '同步媒体库', syncLibraries: '同步媒体库',
syncLibrariesHint: '只有选中的媒体库才会被同步', syncLibrariesHint: '只有选中的媒体库才会被同步',
scanMode: '扫描模式',
scanModeHint: '用于全库刷新和按库刷新:新添加和修改 / 补充缺失 / 覆盖扫描',
verifySsl: '校验 SSL 证书',
verifySslHint: '开启后会校验 HTTPS 证书;如使用自签名证书可关闭',
scanModeOptions: {
newAndModified: '新添加和修改',
supplementMissing: '补充缺失',
fullOverride: '覆盖扫描',
},
nameExists: '【{name}】已存在,请替换为其他名称', nameExists: '【{name}】已存在,请替换为其他名称',
hostRequired: '地址不能为空', hostRequired: '地址不能为空',
apiKeyRequired: 'API密钥不能为空', apiKeyRequired: 'API密钥不能为空',
@@ -3088,7 +3184,7 @@ export default {
title: '媒体服务器', title: '媒体服务器',
description: '配置媒体服务器', description: '配置媒体服务器',
info: '媒体服务器配置说明', info: '媒体服务器配置说明',
infoDesc: '配置媒体服务器用于媒体库管理可选择Emby、JellyfinPlex', infoDesc: '配置媒体服务器用于媒体库管理可选择Emby、JellyfinPlex、飞牛影视或绿联影视',
type: '媒体服务器类型', type: '媒体服务器类型',
typeHint: '选择要使用的媒体服务器类型', typeHint: '选择要使用的媒体服务器类型',
name: '服务器名称', name: '服务器名称',
@@ -3161,3 +3257,7 @@ export default {
}, },
}, },
} }
// Apply patch to add category strings
// This is a temporary placeholder command to show intent.
// I will use replace_file_content to actually edit the file safely.

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: '存儲 & 目錄',
@@ -432,6 +433,8 @@ export default {
config: '配置', config: '配置',
wechat: { wechat: {
name: '企業微信', name: '企業微信',
useBotMode: '使用智能機器人',
useBotModeHint: '開啟後使用智能機器人長連線,固定 dmPolicy=open、groupPolicy=disabled',
corpId: '企業ID', corpId: '企業ID',
corpIdHint: '企業微信後台企業信息中的企業ID', corpIdHint: '企業微信後台企業信息中的企業ID',
corpIdRequired: '企業ID不能為空', corpIdRequired: '企業ID不能為空',
@@ -447,6 +450,15 @@ export default {
tokenHint: '微信企業自建應用->API接收消息配置中的Token', tokenHint: '微信企業自建應用->API接收消息配置中的Token',
encodingAesKey: 'EncodingAESKey', encodingAesKey: 'EncodingAESKey',
encodingAesKeyHint: '微信企業自建應用->API接收消息配置中的EncodingAESKey', encodingAesKeyHint: '微信企業自建應用->API接收消息配置中的EncodingAESKey',
botId: '機器人 BotID',
botIdHint: '企業微信智能機器人的 BotID',
botSecret: '機器人 Secret',
botSecretHint: '企業微信智能機器人長連線專用 Secret',
botChatId: '預設通知目標',
botChatIdHint: '可填寫使用者 userid如需主動發群消息可填寫 group:群聊chatid不填則預設發給已互動使用者',
botChatIdPlaceholder: 'userid 或 group:chatid',
botWsUrl: '長連線地址',
botWsUrlHint: '企業微信智能機器人 WebSocket 位址,通常使用預設值',
admins: '管理員白名單', admins: '管理員白名單',
adminsHint: '可使用管理菜單及命令的用戶ID列表多個ID使用,分隔', adminsHint: '可使用管理菜單及命令的用戶ID列表多個ID使用,分隔',
adminsPlaceholder: '用戶ID列表多個ID使用,分隔', adminsPlaceholder: '用戶ID列表多個ID使用,分隔',
@@ -517,6 +529,21 @@ export default {
usernameHint: '只有對應的用戶登錄後才會推送消息', usernameHint: '只有對應的用戶登錄後才會推送消息',
usernameRequired: '用戶名不能為空', usernameRequired: '用戶名不能為空',
}, },
qqbot: {
name: 'QQ',
appId: 'AppID',
appIdHint: 'QQ 開放平台機器人 AppID',
appIdRequired: 'AppID 不能為空',
appSecret: 'AppSecret',
appSecretHint: 'QQ 開放平台機器人 AppSecret',
appSecretRequired: 'AppSecret 不能為空',
openId: '用戶 OpenID',
openIdHint: '默認接收者 openid單聊用戶需曾與機器人交互過',
openIdPlaceholder: '32位十六進制',
groupOpenId: '群組 OpenID',
groupOpenIdHint: '默認群組 openid群聊與用戶 OpenID 二選一',
groupOpenIdPlaceholder: '群組 openid',
},
}, },
shortcut: { shortcut: {
title: '捷徑', title: '捷徑',
@@ -552,6 +579,10 @@ export default {
title: '緩存', title: '緩存',
subtitle: '管理緩存', subtitle: '管理緩存',
}, },
scheduler: {
title: '服務',
subtitle: '定時服務',
},
}, },
workflow: { workflow: {
components: '動作組件', components: '動作組件',
@@ -1289,6 +1320,9 @@ export default {
llmProviderHint: '選擇使用的LLM服務提供商', llmProviderHint: '選擇使用的LLM服務提供商',
llmModel: 'LLM模型名稱', llmModel: 'LLM模型名稱',
llmModelHint: '指定使用的LLM模型如gpt-3.5-turbo、deepseek-chat等', llmModelHint: '指定使用的LLM模型如gpt-3.5-turbo、deepseek-chat等',
llmMaxContextTokens: 'LLM 最大上下文 Token 數量 (K)',
llmMaxContextTokensHint:
'設定 LLM 記錄會話歷史的最大 Token 數量上限(千),超出後將自動修整歷史記錄以節省 Token 消耗及防止超出 LLM 限制',
llmApiKey: 'LLM API密鑰', llmApiKey: 'LLM API密鑰',
llmApiKeyHint: 'LLM服務提供商的API密鑰用於身份驗證', llmApiKeyHint: 'LLM服務提供商的API密鑰用於身份驗證',
llmApiKeyPlaceholder: '請輸入API密鑰', llmApiKeyPlaceholder: '請輸入API密鑰',
@@ -1296,16 +1330,30 @@ export default {
llmBaseUrlHint: 'LLM API的基礎URL地址用於自定義API端點', llmBaseUrlHint: 'LLM API的基礎URL地址用於自定義API端點',
aiAgentGlobal: '全局智能助手', aiAgentGlobal: '全局智能助手',
aiAgentGlobalHint: '啟用全局智能助手功能,所有消息對話均使用智能體回答而不用使用/ai命令', aiAgentGlobalHint: '啟用全局智能助手功能,所有消息對話均使用智能體回答而不用使用/ai命令',
aiAgentJobInterval: '定時喚醒',
aiAgentJobIntervalHint: '設置定時喚醒的檢查間隔,選擇「不啟用」則不執行定時任務',
aiAgentVerbose: '囉嗦模式',
aiAgentVerboseHint: '開啟後會在智能體回覆時顯示工具調用過程',
aiAgentJobIntervalDisabled: '不啟用',
aiAgentJobInterval1h: '1小時',
aiAgentJobInterval3h: '3小時',
aiAgentJobInterval6h: '6小時',
aiAgentJobInterval12h: '12小時',
aiAgentJobInterval24h: '24小時',
aiAgentJobInterval1w: '1週',
aiAgentJobInterval1M: '1個月',
advancedSettings: '高級設置', advancedSettings: '高級設置',
advancedSettingsDesc: '系統進階設置,特殊情況下才需要調整', advancedSettingsDesc: '系統進階設置,特殊情況下才需要調整',
downloaders: '下載器', downloaders: '下載器',
downloadersDesc: '只有默認下載器才會被默認使用。', downloadersDesc: '只有默認下載器才會被默認使用。',
aiRecommendEnabled: '搜索結果智能推薦', aiRecommendEnabled: '搜索結果智能推薦',
aiRecommendEnabledHint: '啟用搜索結果智能推薦功能,開啟後將在搜索結果頁面顯示智能推薦按鈕,可根據用戶偏好智能推薦資源', aiRecommendEnabledHint:
'啟用搜索結果智能推薦功能,開啟後將在搜索結果頁面顯示智能推薦按鈕,可根據用戶偏好智能推薦資源',
aiRecommendUserPreference: '用戶偏好', aiRecommendUserPreference: '用戶偏好',
aiRecommendUserPreferenceHint: '設置智能推薦時的用戶偏好例如4K WEB-DL Dolby Vision', aiRecommendUserPreferenceHint: '設置智能推薦時的用戶偏好例如4K WEB-DL Dolby Vision',
aiRecommendMaxItems: '智能推薦分析條目上限', aiRecommendMaxItems: '智能推薦分析條目上限',
aiRecommendMaxItemsHint: '限制發送給智能助手進行分析的搜索結果數量,數量越多分析越慢且消耗 Token 越多,建議先手動篩選,篩選出大致範圍後再進行智能推薦', aiRecommendMaxItemsHint:
'限制發送給智能助手進行分析的搜索結果數量,數量越多分析越慢且消耗 Token 越多,建議先手動篩選,篩選出大致範圍後再進行智能推薦',
mediaServers: '媒體服務器', mediaServers: '媒體服務器',
mediaServersDesc: '所有啟用的媒體服務器都會被使用。', mediaServersDesc: '所有啟用的媒體服務器都會被使用。',
trimeMedia: '飛牛影視', trimeMedia: '飛牛影視',
@@ -1328,9 +1376,11 @@ export default {
reloading: '正在應用配置...', reloading: '正在應用配置...',
qbittorrent: 'Qbittorrent', qbittorrent: 'Qbittorrent',
transmission: 'Transmission', transmission: 'Transmission',
rtorrent: 'rTorrent',
emby: 'Emby', emby: 'Emby',
jellyfin: 'Jellyfin', jellyfin: 'Jellyfin',
plex: 'Plex', plex: 'Plex',
ugreen: '綠聯影視',
reloadSuccess: '系統配置已生效', reloadSuccess: '系統配置已生效',
reloadFailed: '重載系統失敗!', reloadFailed: '重載系統失敗!',
auxAuthEnable: '用戶輔助認證', auxAuthEnable: '用戶輔助認證',
@@ -1370,6 +1420,8 @@ export default {
fanartEnableHint: '使用 fanart.tv 的圖片數據', fanartEnableHint: '使用 fanart.tv 的圖片數據',
fanartLang: 'Fanart語言', fanartLang: 'Fanart語言',
fanartLangHint: '設定Fanart圖片的語言偏好多選時按優先級順序排列', fanartLangHint: '設定Fanart圖片的語言偏好多選時按優先級順序排列',
recognizePluginFirst: "優先使用插件識別",
recognizePluginFirstHint: "優先調用插件識別媒體信息,若插件命中則不再調用原生識別",
githubProxy: 'Github加速代理', githubProxy: 'Github加速代理',
githubProxyPlaceholder: '留空表示不使用代理', githubProxyPlaceholder: '留空表示不使用代理',
githubProxyHint: '使用代理加速Github訪問速度', githubProxyHint: '使用代理加速Github訪問速度',
@@ -1480,6 +1532,11 @@ export default {
episodeThumb: '縮略圖', episodeThumb: '縮略圖',
scrapingSwitchSaveFailed: '刮削開關設定保存失敗:{message}', scrapingSwitchSaveFailed: '刮削開關設定保存失敗:{message}',
scrapingSwitchSaveError: '刮削開關設定保存失敗', scrapingSwitchSaveError: '刮削開關設定保存失敗',
policy: {
skipDesc: '跳過刮削,不生成該文件',
missingOnlyDesc: '僅在缺失時刮削,已存在則保持不變',
overwriteDesc: '始終刮削,已存在則覆蓋',
}
}, },
site: { site: {
siteSync: '站點同步', siteSync: '站點同步',
@@ -1574,6 +1631,7 @@ export default {
synologyChat: 'SynologyChat', synologyChat: 'SynologyChat',
voceChat: 'VoceChat', voceChat: 'VoceChat',
webPush: 'WebPush', webPush: 'WebPush',
qq: 'QQ',
custom: '自定義通知', custom: '自定義通知',
}, },
words: { words: {
@@ -1673,6 +1731,25 @@ export default {
storageSaveSuccess: '存儲設置保存成功', storageSaveSuccess: '存儲設置保存成功',
storageSaveFailed: '存儲設置保存失敗!', storageSaveFailed: '存儲設置保存失敗!',
}, },
category: {
title: '分類策略',
subtitle: '配置媒體自動分類規則,按類型、語言、地區等條件自動歸類',
movie: '電影 (Movie)',
tv: '電視劇 (TV)',
name: '分類名稱 (目錄名)',
genre: '內容類型 (Genre)',
language: '語種 (Language)',
languagePlaceholder: '如: zh,cn,en (使用逗號分隔)',
country: '國家/地區 (Country)',
countryPlaceholder: '如: US,CN,JP',
year: '年份 (Year)',
yearPlaceholder: '如: 2023, 2020-2024',
addMovie: '添加電影分類',
addTv: '添加電視劇分類',
saveSuccess: '分類策略保存成功',
loadFailed: '加載分類配置失敗',
saveFailed: '保存失敗: {message}',
},
rule: { rule: {
customRules: '自定義規則', customRules: '自定義規則',
customRulesDesc: '自定義優先級規則項', customRulesDesc: '自定義優先級規則項',
@@ -1972,9 +2049,15 @@ export default {
securityWarningMessage: '分享前請確保工作流沒有敏感資訊比如RSS連結中的PassKey等避免產生資訊洩露。', securityWarningMessage: '分享前請確保工作流沒有敏感資訊比如RSS連結中的PassKey等避免產生資訊洩露。',
}, },
u115Auth: { u115Auth: {
loginTitle: '115網盤登錄', loginTitle: '115網盤授權',
scanQrCode: '請使用微信或115客戶端掃碼', openAuthWindow: '打開授權窗口',
scanned: '已掃碼,請確認登錄', authorizing: '請在新窗口中完成授權...',
authSuccess: '授權成功!',
authFailed: '授權失敗或已過期',
authCanceled: '授權已取消,請重試',
urlEmpty: '授權URL為空',
urlFetchFailed: '獲取授權URL失敗',
popupBlocked: '無法打開授權窗口,請檢查瀏覽器彈窗設置',
complete: '完成', complete: '完成',
reset: '重置', reset: '重置',
}, },
@@ -2586,7 +2669,8 @@ export default {
passkeyManagement: '通行密鑰管理', passkeyManagement: '通行密鑰管理',
registerNewPasskey: '註冊新通行密鑰', registerNewPasskey: '註冊新通行密鑰',
passkeyDescription: '通行密鑰可以讓您無需密碼即可快速安全地登入。', passkeyDescription: '通行密鑰可以讓您無需密碼即可快速安全地登入。',
passkeyAppDescription: '通行密鑰是一種更簡單、更安全的登入方式,可以替代密碼進行登入。您可以使用 iCloud 鑰匙圈、Bitwarden 等支援通行密鑰的應用程式或硬體金鑰完成驗證。', passkeyAppDescription:
'通行密鑰是一種更簡單、更安全的登入方式,可以替代密碼進行登入。您可以使用 iCloud 鑰匙圈、Bitwarden 等支援通行密鑰的應用程式或硬體金鑰完成驗證。',
passkeyName: '通行密鑰名稱', passkeyName: '通行密鑰名稱',
passkeyNamePlaceholder: '例如iPhone、Windows Hello', passkeyNamePlaceholder: '例如iPhone、Windows Hello',
registerPasskey: '註冊通行密鑰', registerPasskey: '註冊通行密鑰',
@@ -2715,6 +2799,7 @@ export default {
name: '名稱', name: '名稱',
type: '類型', type: '類型',
customTypeHint: '自定義下載器類型,用於插件等場景', customTypeHint: '自定義下載器類型,用於插件等場景',
rtorrentHostHint: 'HTTP: http://ip:port/RPC2 或 SCGI: scgi://ip:port',
enabled: '啟用', enabled: '啟用',
default: '預設', default: '預設',
host: '地址', host: '地址',
@@ -2790,6 +2875,15 @@ export default {
password: '密碼', password: '密碼',
syncLibraries: '同步媒體庫', syncLibraries: '同步媒體庫',
syncLibrariesHint: '只有選中的媒體庫才會被同步', syncLibrariesHint: '只有選中的媒體庫才會被同步',
scanMode: '掃描模式',
scanModeHint: '用於全庫刷新和按庫刷新:新添加和修改 / 補充缺失 / 覆蓋掃描',
verifySsl: '校驗 SSL 憑證',
verifySslHint: '開啟後會校驗 HTTPS 憑證;如使用自簽憑證可關閉',
scanModeOptions: {
newAndModified: '新添加和修改',
supplementMissing: '補充缺失',
fullOverride: '覆蓋掃描',
},
nameExists: '【{name}】已存在,請替換為其他名稱', nameExists: '【{name}】已存在,請替換為其他名稱',
}, },
bangumi: { bangumi: {
@@ -3091,7 +3185,7 @@ export default {
title: '媒體伺服器', title: '媒體伺服器',
description: '設定媒體伺服器', description: '設定媒體伺服器',
info: '媒體伺服器設定說明', info: '媒體伺服器設定說明',
infoDesc: '設定媒體伺服器用於媒體庫管理可選擇Emby、JellyfinPlex', infoDesc: '設定媒體伺服器用於媒體庫管理可選擇Emby、JellyfinPlex、飛牛影視或綠聯影視',
type: '媒體伺服器類型', type: '媒體伺服器類型',
typeHint: '選擇要使用的媒體伺服器類型', typeHint: '選擇要使用的媒體伺服器類型',
name: '伺服器名稱', name: '伺服器名稱',

View File

@@ -582,7 +582,7 @@ onUnmounted(() => {
type="text" type="text"
name="username" name="username"
id="username" id="username"
autocomplete="username webauthn" autocomplete="username"
:rules="[requiredValidator]" :rules="[requiredValidator]"
hide-details hide-details
/> />

View File

@@ -6,7 +6,6 @@ import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue' import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue' import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue' import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue' import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue' import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import { getSettingTabs } from '@/router/i18n-menu' import { getSettingTabs } from '@/router/i18n-menu'
@@ -93,15 +92,6 @@ onMounted(() => {
</transition> </transition>
</VWindowItem> </VWindowItem>
<!-- 服务 -->
<VWindowItem value="scheduler">
<transition name="fade-slide" appear>
<div>
<AccountSettingService />
</div>
</transition>
</VWindowItem>
<!-- 通知 --> <!-- 通知 -->
<VWindowItem value="notification"> <VWindowItem value="notification">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>

View File

@@ -185,12 +185,6 @@ export function getSettingTabs(t: Composer['t']) {
tab: 'subscribe', tab: 'subscribe',
description: t('settingTabs.subscribe.description'), description: t('settingTabs.subscribe.description'),
}, },
{
title: t('settingTabs.scheduler.title'),
icon: 'mdi-list-box',
tab: 'scheduler',
description: t('settingTabs.scheduler.description'),
},
{ {
title: t('settingTabs.notification.title'), title: t('settingTabs.notification.title'),
icon: 'mdi-bell', icon: 'mdi-bell',

View File

@@ -80,9 +80,9 @@ async function fetchRemoteModules(): Promise<RemoteModule[]> {
* @param modules 远程模块列表 * @param modules 远程模块列表
*/ */
function injectRemoteModule(module: RemoteModule): void { function injectRemoteModule(module: RemoteModule): void {
// 从浏览器地址栏获取当前地址前缀 // 与 API 请求一致:使用 origin + pathname 作为前缀,子路径代理时 pathname 含 /mp 等
const baseUrl = new URL(window.location.href) const baseUrl = new URL(window.location.href)
// 环境变量 const pathBase = baseUrl.pathname.replace(/\/$/, '') || ''
let apiBase = import.meta.env.VITE_API_BASE_URL let apiBase = import.meta.env.VITE_API_BASE_URL
if (apiBase.startsWith('/')) { if (apiBase.startsWith('/')) {
apiBase = apiBase.slice(1) apiBase = apiBase.slice(1)
@@ -90,8 +90,10 @@ function injectRemoteModule(module: RemoteModule): void {
if (apiBase.endsWith('/')) { if (apiBase.endsWith('/')) {
apiBase = apiBase.slice(0, -1) apiBase = apiBase.slice(0, -1)
} }
const pathWithoutLeadingSlash = module.url.startsWith('/') ? module.url.slice(1) : module.url
const remoteEntryUrl = `${baseUrl.origin}${pathBase}/${apiBase}/${pathWithoutLeadingSlash}`
__federation_method_setRemote(module.id, { __federation_method_setRemote(module.id, {
url: () => Promise.resolve(`${baseUrl.origin}/${apiBase}${module.url}`), url: () => Promise.resolve(remoteEntryUrl),
format: 'esm', format: 'esm',
from: 'vite', from: 'vite',
}) })

View File

@@ -6,10 +6,12 @@
// 导入所有 logo 图标 // 导入所有 logo 图标
import qbittorrentLogo from '@/assets/images/logos/qbittorrent.png' import qbittorrentLogo from '@/assets/images/logos/qbittorrent.png'
import transmissionLogo from '@/assets/images/logos/transmission.png' import transmissionLogo from '@/assets/images/logos/transmission.png'
import rtorrentLogo from '@/assets/images/logos/rtorrent.png'
import embyLogo from '@/assets/images/logos/emby.png' 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.png'
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'
@@ -29,15 +31,18 @@ import pluginLogo from '@/assets/images/logos/plugin.png'
import siteLogo from '@/assets/images/logos/site.webp' import siteLogo from '@/assets/images/logos/site.webp'
import bangumiLogo from '@/assets/images/logos/bangumi.png' import bangumiLogo from '@/assets/images/logos/bangumi.png'
import doubanBlackLogo from '@/assets/images/logos/douban-black.png' import doubanBlackLogo from '@/assets/images/logos/douban-black.png'
import qqLogo from '@/assets/images/logos/qq.png'
// 图标映射表 // 图标映射表
const logoMap: Record<string, string> = { const logoMap: Record<string, string> = {
qbittorrent: qbittorrentLogo, qbittorrent: qbittorrentLogo,
transmission: transmissionLogo, transmission: transmissionLogo,
rtorrent: rtorrentLogo,
emby: embyLogo, emby: embyLogo,
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,
@@ -57,6 +62,7 @@ const logoMap: Record<string, string> = {
site: siteLogo, site: siteLogo,
bangumi: bangumiLogo, bangumi: bangumiLogo,
'douban-black': doubanBlackLogo, 'douban-black': doubanBlackLogo,
qq: qqLogo,
} }
/** /**

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

@@ -31,6 +31,9 @@ const isRefreshed = ref(false)
const dataList = ref<MediaInfo[]>([]) const dataList = ref<MediaInfo[]>([])
const currData = ref<MediaInfo[]>([]) const currData = ref<MediaInfo[]>([])
// 用于保存已处理过的 key
const seenKeys = ref<Set<string>>(new Set<string>())
// 拼装参数 // 拼装参数
function getParams() { function getParams() {
let params = { let params = {
@@ -41,6 +44,31 @@ function getParams() {
return params return params
} }
// MediaInfo 去重的字段
const dedupFields = [
"source",
"type",
"season",
"tmdb_id",
"imdb_id",
"tvdb_id",
"douban_id",
"bangumi_id",
"mediaid_prefix",
"media_id",
] as const;
function deduplicate(items: MediaInfo[]): MediaInfo[] {
return items.filter(item => {
const key = dedupFields.map(field => String(item[field])).join('~');
if (seenKeys.value.has(key)) {
return false;
}
seenKeys.value.add(key);
return true;
});
}
// 获取列表数据 // 获取列表数据
async function fetchData({ done }: { done: any }) { async function fetchData({ done }: { done: any }) {
try { try {
@@ -71,8 +99,10 @@ async function fetchData({ done }: { done: any }) {
done('empty') done('empty')
return return
} }
// 去重
currData.value = deduplicate(currData.value)
// 合并数据 // 合并数据
dataList.value = [...dataList.value, ...currData.value] dataList.value.push(...currData.value)
// 页码+1 // 页码+1
page.value++ page.value++
// 返回加载成功 // 返回加载成功
@@ -92,8 +122,10 @@ async function fetchData({ done }: { done: any }) {
// 如果没有数据,跳出 // 如果没有数据,跳出
done('empty') done('empty')
} else { } else {
// 去重
currData.value = deduplicate(currData.value)
// 合并数据 // 合并数据
dataList.value = [...dataList.value, ...currData.value] dataList.value.push(...currData.value)
// 页码+1 // 页码+1
page.value++ page.value++
// 返回加载成功 // 返回加载成功

View File

@@ -150,7 +150,8 @@ async function loadSeasonEpisodes(season: number) {
// 加载季集信息 // 加载季集信息
if (seasonEpisodesInfo.value[season]) return if (seasonEpisodesInfo.value[season]) return
try { try {
const result: TmdbEpisode[] = await api.get(`tmdb/${mediaDetail.value.tmdb_id}/${season}`) const params = mediaDetail.value.episode_group ? { episode_group: mediaDetail.value.episode_group } : undefined
const result: TmdbEpisode[] = await api.get(`tmdb/${mediaDetail.value.tmdb_id}/${season}`, params ? { params } : undefined)
seasonEpisodesInfo.value[season] = result || [] seasonEpisodesInfo.value[season] = result || []
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -189,7 +190,7 @@ async function checkExists() {
} }
// 查询当前媒体是否已订阅 // 查询当前媒体是否已订阅
async function checkSubscribe(season = 0) { async function checkSubscribe(season: number | null = null) {
try { try {
const mediaid = getMediaId() const mediaid = getMediaId()
@@ -233,9 +234,14 @@ async function checkMovieSubscribed() {
isSubscribed.value = await checkSubscribe() isSubscribed.value = await checkSubscribe()
} }
// 过滤掉第0 // 季列表第0季排在最后
const getMediaSeasons = computed(() => { const getMediaSeasons = computed(() => {
return mediaDetail.value?.season_info?.filter(season => season.season_number !== 0) if (!mediaDetail.value?.season_info) return []
return [...mediaDetail.value.season_info].sort((a, b) => {
if (a.season_number === 0) return 1
if (b.season_number === 0) return -1
return (a.season_number || 0) - (b.season_number || 0)
})
}) })
// 检查所有季的订阅状态 // 检查所有季的订阅状态
@@ -243,7 +249,7 @@ async function checkSeasonsSubscribed() {
if (mediaDetail.value.type !== '电视剧') return if (mediaDetail.value.type !== '电视剧') return
try { try {
mediaDetail.value?.season_info?.forEach(async item => { mediaDetail.value?.season_info?.forEach(async item => {
seasonsSubscribed.value[item.season_number ?? 0] = await checkSubscribe(item.season_number) seasonsSubscribed.value[item.season_number ?? 0] = await checkSubscribe(item.season_number ?? null)
}) })
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -251,13 +257,13 @@ async function checkSeasonsSubscribed() {
} }
// 调用API添加订阅电视剧的话需要指定季 // 调用API添加订阅电视剧的话需要指定季
async function addSubscribe(season = 0) { async function addSubscribe(season: number | null) {
// 开始处理 // 开始处理
startNProgress() startNProgress()
try { try {
// 是否洗版 // 是否洗版
let best_version = existsItemId.value ? 1 : 0 let best_version = existsItemId.value ? 1 : 0
if (season) if (season !== null)
// 全部存在时洗版 // 全部存在时洗版
best_version = !seasonsNotExisted.value[season] ? 1 : 0 best_version = !seasonsNotExisted.value[season] ? 1 : 0
// 请求API // 请求API
@@ -268,7 +274,7 @@ async function addSubscribe(season = 0) {
tmdbid: mediaDetail.value?.tmdb_id, tmdbid: mediaDetail.value?.tmdb_id,
doubanid: mediaDetail.value?.douban_id, doubanid: mediaDetail.value?.douban_id,
bangumiid: mediaDetail.value?.bangumi_id, bangumiid: mediaDetail.value?.bangumi_id,
season, season: mediaDetail.value?.type === '电影' ? null : season,
best_version, best_version,
}) })
@@ -276,7 +282,7 @@ async function addSubscribe(season = 0) {
if (result.success) { if (result.success) {
// 订阅成功 // 订阅成功
isSubscribed.value = true isSubscribed.value = true
if (season) seasonsSubscribed.value[season] = true if (season !== null) seasonsSubscribed.value[season] = true
} }
// 提示 // 提示
@@ -297,8 +303,8 @@ async function addSubscribe(season = 0) {
} }
// 弹出添加订阅提示 // 弹出添加订阅提示
function showSubscribeAddToast(result: boolean, title: string, season: number, message: string, best_version: number) { function showSubscribeAddToast(result: boolean, title: string, season: number | null, message: string, best_version: number) {
if (season) title = `${title} ${formatSeason(season.toString())}` if (season !== null) title = `${title} ${formatSeason(season.toString())}`
let subname = t('media.subscribe.normal') let subname = t('media.subscribe.normal')
if (best_version > 0) subname = t('media.subscribe.bestVersion') if (best_version > 0) subname = t('media.subscribe.bestVersion')
@@ -307,7 +313,7 @@ function showSubscribeAddToast(result: boolean, title: string, season: number, m
} }
// 调用API取消订阅 // 调用API取消订阅
async function removeSubscribe(season: number) { async function removeSubscribe(season: number | null) {
// 开始处理 // 开始处理
startNProgress() startNProgress()
try { try {
@@ -321,7 +327,7 @@ async function removeSubscribe(season: number) {
if (result.success) { if (result.success) {
isSubscribed.value = false isSubscribed.value = false
if (season) seasonsSubscribed.value[season] = false if (season !== null) seasonsSubscribed.value[season] = false
$toast.success(`${mediaDetail.value?.title} ${t('media.subscribe.canceled')}`) $toast.success(`${mediaDetail.value?.title} ${t('media.subscribe.canceled')}`)
} else { } else {
$toast.error(`${mediaDetail.value?.title} ${t('media.subscribe.cancelFailed', { reason: result.message })}`) $toast.error(`${mediaDetail.value?.title} ${t('media.subscribe.cancelFailed', { reason: result.message })}`)
@@ -333,7 +339,7 @@ async function removeSubscribe(season: number) {
} }
// 订阅按钮响应 // 订阅按钮响应
function handleSubscribe(season = 0) { function handleSubscribe(season: number | null = null) {
if (isSubscribed.value) removeSubscribe(season) if (isSubscribed.value) removeSubscribe(season)
else addSubscribe(season) else addSubscribe(season)
} }
@@ -641,7 +647,7 @@ onBeforeMount(() => {
class="ms-2 mb-2" class="ms-2 mb-2"
:color="getSubscribeColor" :color="getSubscribeColor"
variant="tonal" variant="tonal"
@click="handleSubscribe(0)" @click="handleSubscribe()"
> >
<template #prepend> <template #prepend>
<VIcon :icon="getSubscribeIcon" /> <VIcon :icon="getSubscribeIcon" />
@@ -741,8 +747,9 @@ onBeforeMount(() => {
<template #default> <template #default>
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<span class="font-weight-bold">{{ <span class="font-weight-bold">{{
t('media.seasonNumber', { number: season.season_number }) season.season_number === 0 && season.name ?
}}</span> season.name : t('media.seasonNumber', { number: season.season_number })
}}</span>
<VChip size="small" class="ms-1"> <VChip size="small" class="ms-1">
{{ t('media.episodeCount', { count: season.episode_count }) }} {{ t('media.episodeCount', { count: season.episode_count }) }}
</VChip> </VChip>
@@ -754,7 +761,7 @@ onBeforeMount(() => {
class="ms-1" class="ms-1"
:color="seasonsSubscribed[season.season_number || 0] ? 'error' : 'warning'" :color="seasonsSubscribed[season.season_number || 0] ? 'error' : 'warning'"
variant="text" variant="text"
@click.stop="handleSubscribe(season.season_number)" @click.stop="handleSubscribe(season.season_number ?? null)"
> >
<VIcon <VIcon
:icon="seasonsSubscribed[season.season_number || 0] ? 'mdi-heart' : 'mdi-heart-outline'" :icon="seasonsSubscribed[season.season_number || 0] ? 'mdi-heart' : 'mdi-heart-outline'"

View File

@@ -177,7 +177,7 @@ const loading = ref(false)
const totalItems = ref(0) const totalItems = ref(0)
// 是否要分组 // 是否要分组
const group = ref(false) const group = ref<boolean>(route.query.grouped === 'true')
// 分组条件 // 分组条件
const groupBy = ref<any>([ const groupBy = ref<any>([
@@ -487,6 +487,9 @@ function reloadPage(resetPage = false) {
if (currentPage.value) { if (currentPage.value) {
url = addUrlQuery(url, 'currentPage', resetPage ? 1 : currentPage.value) url = addUrlQuery(url, 'currentPage', resetPage ? 1 : currentPage.value)
} }
if (group.value) {
url = addUrlQuery(url, 'grouped', 'true')
}
router.push(url) router.push(url)
} }
@@ -500,6 +503,26 @@ function ensureNumber(value: any, defaultValue: number = 0) {
return value return value
} }
// 按标题分组后的选中数量统计,键为标题,值为对应分组的选中数
const selectedCountsGroupedByTitle = computed(() => {
return selected.value.reduce((acc, item) => {
const title = item.title || '';
acc[title] = (acc[title] || 0) + 1;
return acc;
}, {} as Record<string, number>);
});
// 控制分组内所有子项的选中状态
const toggleGroupSelection = (checked: boolean | null, items: readonly any[]) => {
const values = items.map(item => item.value);
if (checked) {
selected.value = [...new Set([...selected.value, ...values])];
} else {
const itemsSet = new Set(values);
selected.value = selected.value.filter(item => !itemsSet.has(item));
}
};
// 初始加载数据 // 初始加载数据
onMounted(() => { onMounted(() => {
loadStorages() loadStorages()
@@ -563,13 +586,20 @@ onMounted(() => {
<template v-slot:group-header="{ item, columns, toggleGroup, isGroupOpen }"> <template v-slot:group-header="{ item, columns, toggleGroup, isGroupOpen }">
<tr> <tr>
<td :colspan="columns.length"> <td :colspan="columns.length">
<VBtn <div class="d-flex align-center gap-2">
:icon="isGroupOpen(item) ? '$expand' : '$next'" <VBtn
size="small" :icon="isGroupOpen(item) ? '$expand' : '$next'"
variant="text" size="small"
@click="toggleGroup(item)" variant="text"
/> @click="toggleGroup(item)"
{{ item.value }} />
<VCheckbox
:model-value="selectedCountsGroupedByTitle[item.value] == item.items.length"
:indeterminate="selectedCountsGroupedByTitle[item.value] < item.items.length"
@update:modelValue="(checked) => toggleGroupSelection(checked, item.items)"
/>
{{ item.value }}
</div>
</td> </td>
</tr> </tr>
</template> </template>

View File

@@ -8,6 +8,7 @@ import { TransferDirectoryConf, StorageConf } from '@/api/types'
import DirectoryCard from '@/components/cards/DirectoryCard.vue' import DirectoryCard from '@/components/cards/DirectoryCard.vue'
import StorageCard from '@/components/cards/StorageCard.vue' import StorageCard from '@/components/cards/StorageCard.vue'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue' import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import CategoryEditDialog from '@/components/dialog/CategoryEditDialog.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { storageAttributes } from '@/api/constants' import { storageAttributes } from '@/api/constants'
@@ -28,6 +29,9 @@ const $toast = useToast()
// 进度框 // 进度框
const progressDialog = ref(false) const progressDialog = ref(false)
// 分类编辑对话框
const categoryDialog = ref(false)
// 数据源 // 数据源
const sourceItems = [ const sourceItems = [
{ 'title': 'TheMovieDb', 'value': 'themoviedb' }, { 'title': 'TheMovieDb', 'value': 'themoviedb' },
@@ -292,7 +296,12 @@ onMounted(() => {
:directory="element" :directory="element"
:categories="mediaCategories" :categories="mediaCategories"
:storages="storages" :storages="storages"
@update:modelValue="(value: any) => {element.download_path = value?.download; element.library_path = value?.library}" @update:modelValue="
(value: any) => {
element.download_path = value?.download
element.library_path = value?.library
}
"
@close="removeDirectory(element)" @close="removeDirectory(element)"
/> />
</template> </template>
@@ -304,9 +313,13 @@ onMounted(() => {
<VBtn type="submit" @click="saveDirectories" prepend-icon="mdi-content-save"> <VBtn type="submit" @click="saveDirectories" prepend-icon="mdi-content-save">
{{ t('common.save') }} {{ t('common.save') }}
</VBtn> </VBtn>
<VBtn color="success" variant="tonal" @click="addDirectory"> <VBtn color="success" variant="tonal" @click="addDirectory" class="me-2">
<VIcon icon="mdi-plus" /> <VIcon icon="mdi-plus" />
</VBtn> </VBtn>
<VSpacer />
<VBtn color="info" variant="tonal" prepend-icon="mdi-shape-plus" @click="categoryDialog = true">
{{ t('setting.category.title') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>
@@ -370,4 +383,12 @@ onMounted(() => {
</VRow> </VRow>
<!-- 进度框 --> <!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('setting.system.reloading')" /> <ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('setting.system.reloading')" />
<!-- 分类对话框 -->
<CategoryEditDialog
v-if="categoryDialog"
v-model="categoryDialog"
:categories="mediaCategories"
@close="categoryDialog = false"
@done="loadMediaCategories"
/>
</template> </template>

View File

@@ -300,6 +300,9 @@ onMounted(() => {
<VListItem @click="addNotification('synologychat')"> <VListItem @click="addNotification('synologychat')">
<VListItemTitle>{{ t('setting.notification.synologyChat') }}</VListItemTitle> <VListItemTitle>{{ t('setting.notification.synologyChat') }}</VListItemTitle>
</VListItem> </VListItem>
<VListItem @click="addNotification('qqbot')">
<VListItemTitle>{{ t('setting.notification.qq') }}</VListItemTitle>
</VListItem>
<VListItem @click="addNotification('vocechat')"> <VListItem @click="addNotification('vocechat')">
<VListItemTitle>{{ t('setting.notification.voceChat') }}</VListItemTitle> <VListItemTitle>{{ t('setting.notification.voceChat') }}</VListItemTitle>
</VListItem> </VListItem>

View File

@@ -33,6 +33,8 @@ const SystemSettings = ref<any>({
CUSTOMIZE_WALLPAPER_API_URL: null, CUSTOMIZE_WALLPAPER_API_URL: null,
AI_AGENT_ENABLE: false, AI_AGENT_ENABLE: false,
AI_AGENT_GLOBAL: false, AI_AGENT_GLOBAL: false,
AI_AGENT_VERBOSE: false,
AI_AGENT_JOB_INTERVAL: 24,
LLM_PROVIDER: 'deepseek', LLM_PROVIDER: 'deepseek',
LLM_MODEL: 'deepseek-chat', LLM_MODEL: 'deepseek-chat',
LLM_API_KEY: null, LLM_API_KEY: null,
@@ -40,6 +42,7 @@ const SystemSettings = ref<any>({
AI_RECOMMEND_ENABLED: false, AI_RECOMMEND_ENABLED: false,
AI_RECOMMEND_USER_PREFERENCE: null, AI_RECOMMEND_USER_PREFERENCE: null,
AI_RECOMMEND_MAX_ITEMS: 50, AI_RECOMMEND_MAX_ITEMS: 50,
LLM_MAX_CONTEXT_TOKENS: 64,
}, },
// 高级系统设置 // 高级系统设置
Advanced: { Advanced: {
@@ -54,6 +57,7 @@ const SystemSettings = ref<any>({
AUTO_UPDATE_RESOURCE: true, AUTO_UPDATE_RESOURCE: true,
MOVIEPILOT_AUTO_UPDATE: false, MOVIEPILOT_AUTO_UPDATE: false,
// 媒体 // 媒体
RECOGNIZE_PLUGIN_FIRST: false,
TMDB_API_DOMAIN: null, TMDB_API_DOMAIN: null,
TMDB_IMAGE_DOMAIN: null, TMDB_IMAGE_DOMAIN: null,
TMDB_LOCALE: null, TMDB_LOCALE: null,
@@ -83,28 +87,53 @@ const SystemSettings = ref<any>({
}, },
}) })
// 刮削开关设 // 刮削
const ScrapingSwitchs = ref<any>({ const scrapingConfig = [
movie_nfo: true, // 电影NFO {
movie_poster: true, // 电影海报 section: 'movie',
movie_backdrop: true, // 电影背景图 items: [
movie_logo: true, // 电影Logo { key: 'movie_nfo', label: 'setting.system.movieNfo' },
movie_disc: true, // 电影光盘图 { key: 'movie_poster', label: 'setting.system.moviePoster' },
movie_banner: true, // 电影横幅图 { key: 'movie_backdrop', label: 'setting.system.movieBackdrop' },
movie_thumb: true, // 电影缩略图 { key: 'movie_logo', label: 'setting.system.movieLogo' },
tv_nfo: true, // 电视剧NFO { key: 'movie_disc', label: 'setting.system.movieDisc' },
tv_poster: true, // 电视剧海报 { key: 'movie_banner', label: 'setting.system.movieBanner' },
tv_backdrop: true, // 电视剧背景图 { key: 'movie_thumb', label: 'setting.system.movieThumb' },
tv_banner: true, // 电视剧横幅图 ],
tv_logo: true, // 电视剧Logo },
tv_thumb: true, // 电视剧缩略图 {
season_nfo: true, // 季NFO section: 'tv',
season_poster: true, // 季海报 items: [
season_banner: true, // 季横幅图 { key: 'tv_nfo', label: 'setting.system.tvNfo' },
season_thumb: true, // 季缩略图 { key: 'tv_poster', label: 'setting.system.tvPoster' },
episode_nfo: true, // 集NFO { key: 'tv_backdrop', label: 'setting.system.tvBackdrop' },
episode_thumb: true, // 集缩略图 { key: 'tv_banner', label: 'setting.system.tvBanner' },
}) { key: 'tv_logo', label: 'setting.system.tvLogo' },
{ key: 'tv_thumb', label: 'setting.system.tvThumb' },
],
},
{
section: 'season',
items: [
{ key: 'season_nfo', label: 'setting.system.seasonNfo' },
{ key: 'season_poster', label: 'setting.system.seasonPoster' },
{ key: 'season_banner', label: 'setting.system.seasonBanner' },
{ key: 'season_thumb', label: 'setting.system.seasonThumb' },
],
},
{
section: 'episode',
items: [
{ key: 'episode_nfo', label: 'setting.system.episodeNfo' },
{ key: 'episode_thumb', label: 'setting.system.episodeThumb' },
],
},
]
// 刮削策略设置
const ScrapingPolicies = ref<Record<string, 'skip' | 'missingOnly' | 'overwrite'>>(
Object.fromEntries(scrapingConfig.flatMap(section => section.items.map(item => [item.key, 'missingOnly']))),
)
// 是否发送请求的总开关 // 是否发送请求的总开关
const isRequest = ref(true) const isRequest = ref(true)
@@ -479,7 +508,14 @@ async function loadScrapingSwitchs() {
try { try {
const result: { [key: string]: any } = await api.get('system/setting/ScrapingSwitchs') const result: { [key: string]: any } = await api.get('system/setting/ScrapingSwitchs')
if (result.success && result.data?.value) { if (result.success && result.data?.value) {
ScrapingSwitchs.value = { ...ScrapingSwitchs.value, ...result.data.value } const loadedSwitches = result.data.value
for (const key in loadedSwitches) {
if (typeof loadedSwitches[key] === 'boolean') {
// 兼容旧数据
loadedSwitches[key] = loadedSwitches[key] ? 'missingOnly' : 'skip'
}
}
ScrapingPolicies.value = { ...ScrapingPolicies.value, ...loadedSwitches }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@@ -489,7 +525,7 @@ async function loadScrapingSwitchs() {
// 保存刮削开关设置 // 保存刮削开关设置
async function saveScrapingSwitchs() { async function saveScrapingSwitchs() {
try { try {
const result: { [key: string]: any } = await api.post('system/setting/ScrapingSwitchs', ScrapingSwitchs.value) const result: { [key: string]: any } = await api.post('system/setting/ScrapingSwitchs', ScrapingPolicies.value)
if (result.success) { if (result.success) {
return true return true
} else { } else {
@@ -647,7 +683,7 @@ onDeactivated(() => {
</VRow> </VRow>
<VDivider class="my-4" /> <VDivider class="my-4" />
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12" md="4">
<VSwitch <VSwitch
v-model="SystemSettings.Basic.AI_AGENT_ENABLE" v-model="SystemSettings.Basic.AI_AGENT_ENABLE"
:label="t('setting.system.aiAgentEnable')" :label="t('setting.system.aiAgentEnable')"
@@ -655,6 +691,22 @@ onDeactivated(() => {
persistent-hint persistent-hint
/> />
</VCol> </VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="4">
<VSwitch
v-model="SystemSettings.Basic.AI_AGENT_GLOBAL"
:label="t('setting.system.aiAgentGlobal')"
:hint="t('setting.system.aiAgentGlobalHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="4">
<VSwitch
v-model="SystemSettings.Basic.AI_AGENT_VERBOSE"
:label="t('setting.system.aiAgentVerbose')"
:hint="t('setting.system.aiAgentVerboseHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6"> <VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSelect <VSelect
v-model="SystemSettings.Basic.LLM_PROVIDER" v-model="SystemSettings.Basic.LLM_PROVIDER"
@@ -713,14 +765,35 @@ onDeactivated(() => {
</VCombobox> </VCombobox>
</VCol> </VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6"> <VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSwitch <VTextField
v-model="SystemSettings.Basic.AI_AGENT_GLOBAL" v-model.number="SystemSettings.Basic.LLM_MAX_CONTEXT_TOKENS"
:label="t('setting.system.aiAgentGlobal')" :label="t('setting.system.llmMaxContextTokens')"
:hint="t('setting.system.aiAgentGlobalHint')" :hint="t('setting.system.llmMaxContextTokensHint')"
persistent-hint persistent-hint
type="number"
prepend-inner-icon="mdi-counter"
/> />
</VCol> </VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6"> <VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSelect
v-model="SystemSettings.Basic.AI_AGENT_JOB_INTERVAL"
:label="t('setting.system.aiAgentJobInterval')"
:hint="t('setting.system.aiAgentJobIntervalHint')"
persistent-hint
:items="[
{ title: t('setting.system.aiAgentJobIntervalDisabled'), value: 0 },
{ title: t('setting.system.aiAgentJobInterval1h'), value: 1 },
{ title: t('setting.system.aiAgentJobInterval3h'), value: 3 },
{ title: t('setting.system.aiAgentJobInterval6h'), value: 6 },
{ title: t('setting.system.aiAgentJobInterval12h'), value: 12 },
{ title: t('setting.system.aiAgentJobInterval24h'), value: 24 },
{ title: t('setting.system.aiAgentJobInterval1w'), value: 168 },
{ title: t('setting.system.aiAgentJobInterval1M'), value: 720 },
]"
prepend-inner-icon="mdi-timer-outline"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12">
<VSwitch <VSwitch
v-model="SystemSettings.Basic.AI_RECOMMEND_ENABLED" v-model="SystemSettings.Basic.AI_RECOMMEND_ENABLED"
:label="t('setting.system.aiRecommendEnabled')" :label="t('setting.system.aiRecommendEnabled')"
@@ -728,7 +801,11 @@ onDeactivated(() => {
persistent-hint persistent-hint
/> />
</VCol> </VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.AI_RECOMMEND_ENABLED" cols="12"> <VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.AI_RECOMMEND_ENABLED"
cols="12"
md="6"
>
<VTextarea <VTextarea
v-model="SystemSettings.Basic.AI_RECOMMEND_USER_PREFERENCE" v-model="SystemSettings.Basic.AI_RECOMMEND_USER_PREFERENCE"
:label="t('setting.system.aiRecommendUserPreference')" :label="t('setting.system.aiRecommendUserPreference')"
@@ -1087,6 +1164,16 @@ onDeactivated(() => {
/> />
</VCol> </VCol>
</VRow> </VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.RECOGNIZE_PLUGIN_FIRST"
:label="t('setting.system.recognizePluginFirst')"
:hint="t('setting.system.recognizePluginFirstHint')"
persistent-hint
/>
</VCol>
</VRow>
<!-- 刮削开关设置 --> <!-- 刮削开关设置 -->
<VRow class="mt-4"> <VRow class="mt-4">
@@ -1094,173 +1181,67 @@ onDeactivated(() => {
<VExpansionPanels> <VExpansionPanels>
<VExpansionPanel> <VExpansionPanel>
<VExpansionPanelTitle class="text-lg"> <VExpansionPanelTitle class="text-lg">
<template #default> <VIcon icon="mdi-checkbox-multiple-outline" class="me-2" />
<VIcon icon="mdi-checkbox-multiple-outline" class="me-2" /> {{ t('setting.system.scrapingSwitchSettings') }}
{{ t('setting.system.scrapingSwitchSettings') }} <!-- 帮助图标 -->
</template> <VTooltip location="bottom" open-delay="200">
<template #activator="{ props: tooltipProps }">
<VBtn
v-bind="tooltipProps"
icon="mdi-help-circle"
size="small"
variant="text"
color="medium-emphasis"
class="ml-2"
@click.stop
/>
</template>
<div class="d-flex flex-column gap-2 py-2">
<div class="d-flex align-center">
<VIcon icon="mdi-file-remove" color="error" class="mr-2" />
<span>{{ t('setting.system.policy.skipDesc') }}</span>
</div>
<div class="d-flex align-center">
<VIcon icon="mdi-file-plus" color="success" class="mr-2" />
<span>{{ t('setting.system.policy.missingOnlyDesc') }}</span>
</div>
<div class="d-flex align-center">
<VIcon icon="mdi-file-replace" color="primary" class="mr-2" />
<span>{{ t('setting.system.policy.overwriteDesc') }}</span>
</div>
</div>
</VTooltip>
</VExpansionPanelTitle> </VExpansionPanelTitle>
<VExpansionPanelText> <VExpansionPanelText>
<VRow> <VRow v-for="section in scrapingConfig" :key="section.section">
<VCol cols="12" class="pb-2"> <VCol cols="12" class="pb-2">
<VListSubheader class="text-lg">{{ t('setting.system.movie') }}</VListSubheader> <VListSubheader class="text-lg">
{{ t(`setting.system.${section.section}`) }}
</VListSubheader>
</VCol> </VCol>
<VCol cols="6" md="3"> <VCol v-for="item in section.items" :key="item.key" cols="12" md="4">
<VCheckbox <div class="d-flex align-center">
v-model="ScrapingSwitchs.movie_nfo" <VBtnToggle
:label="t('setting.system.movieNfo')" :model-value="ScrapingPolicies[item.key]"
density="compact" @update:model-value="ScrapingPolicies[item.key] = $event"
/> color="primary"
</VCol> variant="tonal"
<VCol cols="6" md="3"> rounded="lg"
<VCheckbox >
v-model="ScrapingSwitchs.movie_poster" <VBtn value="skip" color="error">
:label="t('setting.system.moviePoster')" <VIcon icon="mdi-file-remove" />
density="compact" </VBtn>
/> <VBtn value="missingOnly" color="success">
</VCol> <VIcon icon="mdi-file-plus" />
<VCol cols="6" md="3"> </VBtn>
<VCheckbox <VBtn value="overwrite" color="primary">
v-model="ScrapingSwitchs.movie_backdrop" <VIcon icon="mdi-file-replace" />
:label="t('setting.system.movieBackdrop')" </VBtn>
density="compact" </VBtnToggle>
/> <span class="ml-2">{{ t(item.label) }}</span>
</VCol> </div>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.movie_logo"
:label="t('setting.system.movieLogo')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.movie_disc"
:label="t('setting.system.movieDisc')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.movie_banner"
:label="t('setting.system.movieBanner')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.movie_thumb"
:label="t('setting.system.movieThumb')"
density="compact"
/>
</VCol>
</VRow>
<VDivider class="my-4" />
<VRow>
<VCol cols="12" class="pb-2">
<VListSubheader class="text-lg">{{ t('setting.system.tv') }}</VListSubheader>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.tv_nfo"
:label="t('setting.system.tvNfo')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.tv_poster"
:label="t('setting.system.tvPoster')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.tv_backdrop"
:label="t('setting.system.tvBackdrop')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.tv_banner"
:label="t('setting.system.tvBanner')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.tv_logo"
:label="t('setting.system.tvLogo')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.tv_thumb"
:label="t('setting.system.tvThumb')"
density="compact"
/>
</VCol>
</VRow>
<VDivider class="my-4" />
<VRow>
<VCol cols="12" class="pb-2">
<VListSubheader class="text-lg">{{ t('setting.system.season') }}</VListSubheader>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.season_nfo"
:label="t('setting.system.seasonNfo')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.season_poster"
:label="t('setting.system.seasonPoster')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.season_banner"
:label="t('setting.system.seasonBanner')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.season_thumb"
:label="t('setting.system.seasonThumb')"
density="compact"
/>
</VCol>
</VRow>
<VDivider class="my-4" />
<VRow>
<VCol cols="12" class="pb-2">
<VListSubheader class="text-lg">{{ t('setting.system.episode') }}</VListSubheader>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.episode_nfo"
:label="t('setting.system.episodeNfo')"
density="compact"
/>
</VCol>
<VCol cols="6" md="3">
<VCheckbox
v-model="ScrapingSwitchs.episode_thumb"
:label="t('setting.system.episodeThumb')"
density="compact"
/>
</VCol> </VCol>
<VDivider v-if="section.section !== 'episode'" class="my-4" />
</VRow> </VRow>
</VExpansionPanelText> </VExpansionPanelText>
</VExpansionPanel> </VExpansionPanel>
@@ -1414,7 +1395,10 @@ onDeactivated(() => {
min="1" min="1"
type="number" type="number"
:suffix="t('setting.system.mb')" :suffix="t('setting.system.mb')"
:rules="[(v: any) => v === 0 || !!v || t('setting.system.logMaxFileSizeRequired'), (v: any) => v >= 1 || t('setting.system.logMaxFileSizeMin')]" :rules="[
(v: any) => v === 0 || !!v || t('setting.system.logMaxFileSizeRequired'),
(v: any) => v >= 1 || t('setting.system.logMaxFileSizeMin'),
]"
prepend-inner-icon="mdi-file-document" prepend-inner-icon="mdi-file-document"
/> />
</VCol> </VCol>
@@ -1426,7 +1410,10 @@ onDeactivated(() => {
persistent-hint persistent-hint
min="1" min="1"
type="number" type="number"
:rules="[(v: any) => v === 0 || !!v || t('setting.system.logBackupCountRequired'), (v: any) => v >= 1 || t('setting.system.logBackupCountMin')]" :rules="[
(v: any) => v === 0 || !!v || t('setting.system.logBackupCountRequired'),
(v: any) => v >= 1 || t('setting.system.logBackupCountMin'),
]"
prepend-inner-icon="mdi-backup-restore" prepend-inner-icon="mdi-backup-restore"
/> />
</VCol> </VCol>

View File

@@ -27,7 +27,7 @@ const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
<div class="mb-4"> <div class="mb-4">
<h4 class="text-h6 mb-4">{{ t('setupWizard.downloader.type') }}</h4> <h4 class="text-h6 mb-4">{{ t('setupWizard.downloader.type') }}</h4>
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="4">
<VCard <VCard
:color="wizardData.downloader.type === 'qbittorrent' ? 'primary' : 'default'" :color="wizardData.downloader.type === 'qbittorrent' ? 'primary' : 'default'"
:variant="wizardData.downloader.type === 'qbittorrent' ? 'tonal' : 'outlined'" :variant="wizardData.downloader.type === 'qbittorrent' ? 'tonal' : 'outlined'"
@@ -40,7 +40,7 @@ const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
</VCardText> </VCardText>
</VCard> </VCard>
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="4">
<VCard <VCard
:color="wizardData.downloader.type === 'transmission' ? 'primary' : 'default'" :color="wizardData.downloader.type === 'transmission' ? 'primary' : 'default'"
:variant="wizardData.downloader.type === 'transmission' ? 'tonal' : 'outlined'" :variant="wizardData.downloader.type === 'transmission' ? 'tonal' : 'outlined'"
@@ -53,6 +53,19 @@ const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
</VCardText> </VCardText>
</VCard> </VCard>
</VCol> </VCol>
<VCol cols="12" md="4">
<VCard
:color="wizardData.downloader.type === 'rtorrent' ? 'primary' : 'default'"
:variant="wizardData.downloader.type === 'rtorrent' ? 'tonal' : 'outlined'"
class="cursor-pointer"
@click="selectDownloader('rtorrent')"
>
<VCardText class="text-center">
<VImg :src="getLogoUrl('rtorrent')" height="48" width="48" class="mx-auto mb-2" />
<div class="text-h6">rTorrent</div>
</VCardText>
</VCard>
</VCol>
</VRow> </VRow>
</div> </div>
</VCol> </VCol>
@@ -203,6 +216,63 @@ const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
/> />
</VCol> </VCol>
</VRow> </VRow>
<VRow v-else-if="wizardData.downloader.type === 'rtorrent'">
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.downloader.name"
:label="t('downloader.name')"
:placeholder="t('downloader.nameRequired')"
:hint="t('downloader.name')"
:error="validationErrors.downloader.name"
:error-messages="validationErrors.downloader.name ? [t('downloader.nameRequired')] : []"
persistent-hint
active
prepend-inner-icon="mdi-label"
required
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.downloader.config.host"
:label="t('downloader.host')"
placeholder="http(s)://ip:port/RPC2"
:hint="t('downloader.rtorrentHostHint')"
:error="validationErrors.downloader.host"
:error-messages="validationErrors.downloader.host ? [t('downloader.hostRequired')] : []"
persistent-hint
active
prepend-inner-icon="mdi-server"
required
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.downloader.config.username"
:label="t('downloader.username')"
:hint="t('downloader.username')"
:error="validationErrors.downloader.username"
:error-messages="validationErrors.downloader.username ? [t('downloader.usernameRequired')] : []"
persistent-hint
active
prepend-inner-icon="mdi-account"
required
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.downloader.config.password"
type="password"
:label="t('downloader.password')"
:hint="t('downloader.password')"
:error="validationErrors.downloader.password"
:error-messages="validationErrors.downloader.password ? [t('downloader.passwordRequired')] : []"
persistent-hint
active
prepend-inner-icon="mdi-lock"
required
/>
</VCol>
</VRow>
<VRow v-else> <VRow v-else>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField

View File

@@ -15,6 +15,23 @@ 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 ensureUgreenConfig() {
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'
}
if (wizardData.value.mediaServer.config.verify_ssl === undefined) {
wizardData.value.mediaServer.config.verify_ssl = true
}
}
// 调用API查询媒体库 // 调用API查询媒体库
async function loadLibrary(server: string) { async function loadLibrary(server: string) {
try { try {
@@ -42,6 +59,7 @@ async function loadLibrary(server: string) {
// 选择媒体服务器并自动加载媒体库 // 选择媒体服务器并自动加载媒体库
async function selectMediaServerWithLibrary(type: string) { async function selectMediaServerWithLibrary(type: string) {
selectMediaServer(type) selectMediaServer(type)
ensureUgreenConfig()
// 如果选择了媒体服务器类型,自动加载媒体库 // 如果选择了媒体服务器类型,自动加载媒体库
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 +68,7 @@ async function selectMediaServerWithLibrary(type: string) {
// 组件挂载时检查是否需要加载媒体库 // 组件挂载时检查是否需要加载媒体库
onMounted(async () => { onMounted(async () => {
ensureUgreenConfig()
// 如果已经有媒体服务器配置,自动加载媒体库 // 如果已经有媒体服务器配置,自动加载媒体库
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 +79,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]) => {
ensureUgreenConfig()
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 +161,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 +413,107 @@ 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>
<VCol cols="12" md="6">
<VSwitch
v-model="wizardData.mediaServer.config.verify_ssl"
:label="t('mediaserver.verifySsl')"
:hint="t('mediaserver.verifySslHint')"
persistent-hint
color="primary"
inset
/>
</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

View File

@@ -91,6 +91,19 @@ const notificationTypes = [
</VCardText> </VCardText>
</VCard> </VCard>
</VCol> </VCol>
<VCol cols="12" md="3">
<VCard
:color="wizardData.notification.type === 'qqbot' ? 'primary' : 'default'"
:variant="wizardData.notification.type === 'qqbot' ? 'tonal' : 'outlined'"
class="cursor-pointer"
@click="selectNotification('qqbot')"
>
<VCardText class="text-center">
<VImg :src="getLogoUrl('notification')" height="48" width="48" class="mx-auto mb-2" />
<div class="text-h6">QQ</div>
</VCardText>
</VCard>
</VCol>
<VCol cols="12" md="3"> <VCol cols="12" md="3">
<VCard <VCard
:color="wizardData.notification.type === 'vocechat' ? 'primary' : 'default'" :color="wizardData.notification.type === 'vocechat' ? 'primary' : 'default'"
@@ -312,6 +325,59 @@ const notificationTypes = [
/> />
</VCol> </VCol>
</VRow> </VRow>
<VRow v-else-if="wizardData.notification.type === 'qqbot'">
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.notification.name"
:label="t('notification.name')"
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
:error="validationErrors.notification.name"
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
persistent-hint
prepend-inner-icon="mdi-label"
required
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.notification.config.QQ_APP_ID"
:label="t('notification.qqbot.appId')"
:hint="t('notification.qqbot.appIdHint')"
persistent-hint
prepend-inner-icon="mdi-application"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.notification.config.QQ_APP_SECRET"
:label="t('notification.qqbot.appSecret')"
:hint="t('notification.qqbot.appSecretHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.notification.config.QQ_OPENID"
:label="t('notification.qqbot.openId')"
:placeholder="t('notification.qqbot.openIdPlaceholder')"
:hint="t('notification.qqbot.openIdHint')"
persistent-hint
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.notification.config.QQ_GROUP_OPENID"
:label="t('notification.qqbot.groupOpenId')"
:placeholder="t('notification.qqbot.groupOpenIdPlaceholder')"
:hint="t('notification.qqbot.groupOpenIdHint')"
persistent-hint
prepend-inner-icon="mdi-account-group"
/>
</VCol>
</VRow>
<VRow v-else-if="wizardData.notification.type === 'slack'"> <VRow v-else-if="wizardData.notification.type === 'slack'">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField

View File

@@ -71,7 +71,8 @@ async function eventsHander(subscribe: Subscribe) {
} }
} else { } else {
// 调用API查询集信息 // 调用API查询集信息
const episodes: TmdbEpisode[] = await api.get(`tmdb/${subscribe.tmdbid}/${subscribe.season}`) const params = subscribe.episode_group ? { episode_group: subscribe.episode_group } : undefined
const episodes: TmdbEpisode[] = await api.get(`tmdb/${subscribe.tmdbid}/${subscribe.season}`, params ? { params } : undefined)
interface EpisodeInfo { interface EpisodeInfo {
title: string title: string

View File

@@ -70,10 +70,6 @@ useDataRefresh(
<template> <template>
<VCard> <VCard>
<VCardItem>
<VCardTitle>{{ t('setting.scheduler.title') }}</VCardTitle>
<VCardSubtitle>{{ t('setting.scheduler.subtitle') }}</VCardSubtitle>
</VCardItem>
<VTable class="text-no-wrap"> <VTable class="text-no-wrap">
<thead> <thead>
<tr> <tr>

View File

@@ -132,6 +132,7 @@ async function fetchUserInfo() {
if (result) { if (result) {
accountInfo.value = result accountInfo.value = result
accountInfo.value.avatar = accountInfo.value.avatar ? accountInfo.value.avatar : avatar1 accountInfo.value.avatar = accountInfo.value.avatar ? accountInfo.value.avatar : avatar1
accountInfo.value.nickname = accountInfo.value.settings?.nickname ?? ''
currentUserName.value = accountInfo.value.name currentUserName.value = accountInfo.value.name
currentAvatar.value = accountInfo.value.avatar currentAvatar.value = accountInfo.value.avatar
// 同时加载PassKey列表 // 同时加载PassKey列表
@@ -161,12 +162,10 @@ async function saveAccountInfo() {
} }
// 将nickname保存到settings中后端可以直接处理JSON对象 // 将nickname保存到settings中后端可以直接处理JSON对象
if (accountInfo.value.nickname) { if (!accountInfo.value.settings) {
if (!accountInfo.value.settings) { accountInfo.value.settings = {}
accountInfo.value.settings = {}
}
accountInfo.value.settings.nickname = accountInfo.value.nickname
} }
accountInfo.value.settings.nickname = accountInfo.value.nickname ?? ''
const oldUserName = accountInfo.value.name const oldUserName = accountInfo.value.name
const oldAvatar = accountInfo.value.avatar const oldAvatar = accountInfo.value.avatar

View File

@@ -861,24 +861,24 @@
integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg== integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==
"@emnapi/core@^1.4.3": "@emnapi/core@^1.4.3":
version "1.4.5" version "1.8.1"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.5.tgz#bfbb0cbbbb9f96ec4e2c4fd917b7bbe5495ceccb" resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.8.1.tgz#fd9efe721a616288345ffee17a1f26ac5dd01349"
integrity sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q== integrity sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==
dependencies: dependencies:
"@emnapi/wasi-threads" "1.0.4" "@emnapi/wasi-threads" "1.1.0"
tslib "^2.4.0" tslib "^2.4.0"
"@emnapi/runtime@^1.2.0", "@emnapi/runtime@^1.4.3": "@emnapi/runtime@^1.2.0", "@emnapi/runtime@^1.4.3":
version "1.4.5" version "1.8.1"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.5.tgz#c67710d0661070f38418b6474584f159de38aba9" resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.8.1.tgz#550fa7e3c0d49c5fb175a116e8cd70614f9a22a5"
integrity sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg== integrity sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"
"@emnapi/wasi-threads@1.0.4": "@emnapi/wasi-threads@1.1.0":
version "1.0.4" version "1.1.0"
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz#703fc094d969e273b1b71c292523b2f792862bf4" resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
integrity sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g== integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"
@@ -1147,21 +1147,21 @@
"@iconify-json/line-md@^1.2.13": "@iconify-json/line-md@^1.2.13":
version "1.2.13" version "1.2.13"
resolved "https://registry.yarnpkg.com/@iconify-json/line-md/-/line-md-1.2.13.tgz#19714b8471ebac5871e20036512eaffa869a04b7" resolved "https://registry.npmjs.org/@iconify-json/line-md/-/line-md-1.2.13.tgz"
integrity sha512-XFXThXsEQ2Wzzn+ze2T1d+JHkkFvI1AxiVKnOox4qFbdR9EVikckZlUK+/DUsV4zSy6pMQAgXpIk+1xG8qFYPQ== integrity sha512-XFXThXsEQ2Wzzn+ze2T1d+JHkkFvI1AxiVKnOox4qFbdR9EVikckZlUK+/DUsV4zSy6pMQAgXpIk+1xG8qFYPQ==
dependencies: dependencies:
"@iconify/types" "*" "@iconify/types" "*"
"@iconify-json/lucide@^1.2.85": "@iconify-json/lucide@^1.2.85":
version "1.2.85" version "1.2.85"
resolved "https://registry.yarnpkg.com/@iconify-json/lucide/-/lucide-1.2.85.tgz#0074b64f50798da4b89f9f74e4db5a4e56c640b1" resolved "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.85.tgz"
integrity sha512-VXUWT6KRDiVK4Ty/7Ypu+U0KnSbHzDAOOiSgLLPhU8u3ES5IusP1X7ahZb1iwiVKGWRG6gkKywaRUIZLgYWXyA== integrity sha512-VXUWT6KRDiVK4Ty/7Ypu+U0KnSbHzDAOOiSgLLPhU8u3ES5IusP1X7ahZb1iwiVKGWRG6gkKywaRUIZLgYWXyA==
dependencies: dependencies:
"@iconify/types" "*" "@iconify/types" "*"
"@iconify-json/material-symbols@^1.2.51": "@iconify-json/material-symbols@^1.2.51":
version "1.2.51" version "1.2.51"
resolved "https://registry.yarnpkg.com/@iconify-json/material-symbols/-/material-symbols-1.2.51.tgz#270862a21bb65a8632de4943146096b5a58863ae" resolved "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.51.tgz"
integrity sha512-GkxlK8ocHi3NVVozaW62jm3qR9fNY3xX2penFtIRvoe1OtNhJ2KD4KRzv8x34pugMOAZYK8sALMcU30gDgCi1A== integrity sha512-GkxlK8ocHi3NVVozaW62jm3qR9fNY3xX2penFtIRvoe1OtNhJ2KD4KRzv8x34pugMOAZYK8sALMcU30gDgCi1A==
dependencies: dependencies:
"@iconify/types" "*" "@iconify/types" "*"
@@ -1942,20 +1942,20 @@
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@tybys/wasm-util@^0.10.0": "@tybys/wasm-util@^0.10.0":
version "0.10.0" version "0.10.1"
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369" resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
integrity sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ== integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"
"@types/body-scroll-lock@^3.1.2": "@types/body-scroll-lock@^3.1.2":
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/@types/body-scroll-lock/-/body-scroll-lock-3.1.2.tgz#1ae7857d98180dbe6c3b05abbe7ec1fa67b614e3" resolved "https://registry.npmjs.org/@types/body-scroll-lock/-/body-scroll-lock-3.1.2.tgz"
integrity sha512-ELhtuphE/YbhEcpBf/rIV9Tl3/O0A0gpCVD+oYFSS8bWstHFJUgA4nNw1ZakVlRC38XaQEIsBogUZKWIPBvpfQ== integrity sha512-ELhtuphE/YbhEcpBf/rIV9Tl3/O0A0gpCVD+oYFSS8bWstHFJUgA4nNw1ZakVlRC38XaQEIsBogUZKWIPBvpfQ==
"@types/crypto-js@^4.2.2": "@types/crypto-js@^4.2.2":
version "4.2.2" version "4.2.2"
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea" resolved "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz"
integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ== integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==
"@types/debug@^4.1.12": "@types/debug@^4.1.12":
@@ -1999,7 +1999,7 @@
"@types/linkify-it@^5": "@types/linkify-it@^5":
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz"
integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
"@types/lodash-es@^4.17.12": "@types/lodash-es@^4.17.12":
@@ -2016,14 +2016,14 @@
"@types/markdown-it-link-attributes@^3.0.5": "@types/markdown-it-link-attributes@^3.0.5":
version "3.0.5" version "3.0.5"
resolved "https://registry.yarnpkg.com/@types/markdown-it-link-attributes/-/markdown-it-link-attributes-3.0.5.tgz#521179990cd2ced55761d9b8c93e502b679df329" resolved "https://registry.npmjs.org/@types/markdown-it-link-attributes/-/markdown-it-link-attributes-3.0.5.tgz"
integrity sha512-VZ2BGN3ywUg7mBD8W6PwR8ChpOxaQSBDbLqPgvNI+uIra3zY2af1eG/3XzWTKjEraTWskMKnZqZd6m1fDF67Bg== integrity sha512-VZ2BGN3ywUg7mBD8W6PwR8ChpOxaQSBDbLqPgvNI+uIra3zY2af1eG/3XzWTKjEraTWskMKnZqZd6m1fDF67Bg==
dependencies: dependencies:
"@types/markdown-it" "*" "@types/markdown-it" "*"
"@types/markdown-it@*", "@types/markdown-it@^14.1.2": "@types/markdown-it@*", "@types/markdown-it@^14.1.2":
version "14.1.2" version "14.1.2"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz"
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
dependencies: dependencies:
"@types/linkify-it" "^5" "@types/linkify-it" "^5"
@@ -2031,7 +2031,7 @@
"@types/mdurl@^2": "@types/mdurl@^2":
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz"
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
"@types/mousetrap@^1.6.15": "@types/mousetrap@^1.6.15":
@@ -2068,7 +2068,7 @@
"@types/qrcode@^1.5.6": "@types/qrcode@^1.5.6":
version "1.5.6" version "1.5.6"
resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.6.tgz#07c33cb9ec0ad88be4636e636e28e54d99b65f42" resolved "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz"
integrity sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw== integrity sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
@@ -2939,7 +2939,7 @@ body-parser@1.20.3:
body-scroll-lock@^3.1.5: body-scroll-lock@^3.1.5:
version "3.1.5" version "3.1.5"
resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz#c1392d9217ed2c3e237fee1e910f6cdd80b7aaec" resolved "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz"
integrity sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg== integrity sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==
boolbase@^1.0.0: boolbase@^1.0.0:
@@ -3071,7 +3071,7 @@ camelcase-css@^2.0.1:
camelcase@^5.0.0: camelcase@^5.0.0:
version "5.3.1" version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702: caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
@@ -3164,7 +3164,7 @@ clean-regexp@^1.0.0:
cliui@^6.0.0: cliui@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz"
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
dependencies: dependencies:
string-width "^4.2.0" string-width "^4.2.0"
@@ -3330,7 +3330,7 @@ cross-spawn@^7.0.6:
crypto-js@^4.2.0: crypto-js@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz"
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
crypto-random-string@^2.0.0: crypto-random-string@^2.0.0:
@@ -3529,7 +3529,7 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3
decamelize@^1.2.0: decamelize@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
deep-is@^0.1.3: deep-is@^0.1.3:
@@ -3617,7 +3617,7 @@ didyoumean@^1.2.2:
dijkstrajs@^1.0.1: dijkstrajs@^1.0.1:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" resolved "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz"
integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
dir-glob@^3.0.1: dir-glob@^3.0.1:
@@ -4531,7 +4531,7 @@ gensync@^1.0.0-beta.2:
get-caller-file@^2.0.1: get-caller-file@^2.0.1:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0:
@@ -5397,7 +5397,7 @@ lines-and-columns@^1.1.6:
linkify-it@^5.0.0: linkify-it@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz"
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
dependencies: dependencies:
uc.micro "^2.0.0" uc.micro "^2.0.0"
@@ -5505,12 +5505,12 @@ magic-string@^0.30.11, magic-string@^0.30.17:
markdown-it-link-attributes@^4.0.1: markdown-it-link-attributes@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz#25751f2cf74fd91f0a35ba7b3247fa45f2056d88" resolved "https://registry.npmjs.org/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz"
integrity sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ== integrity sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==
markdown-it@^14.1.0: markdown-it@^14.1.0:
version "14.1.0" version "14.1.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz"
integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
dependencies: dependencies:
argparse "^2.0.1" argparse "^2.0.1"
@@ -5552,7 +5552,7 @@ mdn-data@^2.15.0:
mdurl@^2.0.0: mdurl@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz"
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
media-typer@0.3.0: media-typer@0.3.0:
@@ -6158,7 +6158,7 @@ pluralize@^8.0.0:
pngjs@^5.0.0: pngjs@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" resolved "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz"
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
possible-typed-array-names@^1.0.0: possible-typed-array-names@^1.0.0:
@@ -6315,7 +6315,7 @@ pump@^3.0.0:
punycode.js@^2.3.1: punycode.js@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" resolved "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz"
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
punycode@^2.1.0: punycode@^2.1.0:
@@ -6325,7 +6325,7 @@ punycode@^2.1.0:
qrcode@^1.5.4: qrcode@^1.5.4:
version "1.5.4" version "1.5.4"
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88" resolved "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz"
integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg== integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==
dependencies: dependencies:
dijkstrajs "^1.0.1" dijkstrajs "^1.0.1"
@@ -6538,7 +6538,7 @@ regjsparser@^0.12.0:
require-directory@^2.1.1: require-directory@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
require-from-string@^2.0.2: require-from-string@^2.0.2:
@@ -6548,7 +6548,7 @@ require-from-string@^2.0.2:
require-main-filename@^2.0.0: require-main-filename@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
requires-port@^1.0.0: requires-port@^1.0.0:
@@ -6754,7 +6754,7 @@ serve-static@1.16.2:
set-blocking@^2.0.0: set-blocking@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
set-function-length@^1.2.2: set-function-length@^1.2.2:
@@ -7548,7 +7548,7 @@ typescript@^5, typescript@^5.0.4:
uc.micro@^2.0.0, uc.micro@^2.1.0: uc.micro@^2.0.0, uc.micro@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz"
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
ufo@^1.5.4, ufo@^1.6.1: ufo@^1.5.4, ufo@^1.6.1:
@@ -8062,7 +8062,7 @@ which-collection@^1.0.2:
which-module@^2.0.0: which-module@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz"
integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
which-typed-array@^1.1.16, which-typed-array@^1.1.18: which-typed-array@^1.1.16, which-typed-array@^1.1.18:
@@ -8266,7 +8266,7 @@ workbox-window@7.3.0, workbox-window@^7.3.0:
wrap-ansi@^6.2.0: wrap-ansi@^6.2.0:
version "6.2.0" version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz"
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
dependencies: dependencies:
ansi-styles "^4.0.0" ansi-styles "^4.0.0"
@@ -8302,7 +8302,7 @@ xml-name-validator@^4.0.0:
y18n@^4.0.0: y18n@^4.0.0:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz"
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
yallist@^3.0.2: yallist@^3.0.2:
@@ -8330,7 +8330,7 @@ yaml@^2.0.0, yaml@^2.3.4, yaml@^2.7.0:
yargs-parser@^18.1.2: yargs-parser@^18.1.2:
version "18.1.3" version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz"
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies: dependencies:
camelcase "^5.0.0" camelcase "^5.0.0"
@@ -8338,7 +8338,7 @@ yargs-parser@^18.1.2:
yargs@^15.3.1: yargs@^15.3.1:
version "15.4.1" version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
dependencies: dependencies:
cliui "^6.0.0" cliui "^6.0.0"