Compare commits

...

47 Commits

Author SHA1 Message Date
jxxghp
31047b0d44 优化账户设置缓存页面的筛选条件 2025-05-30 17:01:47 +08:00
jxxghp
7c2b724d10 fix ui 2025-05-30 09:04:15 +08:00
jxxghp
ca5670f06b v2.5.2 2025-05-30 08:48:39 +08:00
jxxghp
427e05871d 调整SubscribeCard组件的样式 2025-05-30 08:32:16 +08:00
jxxghp
bef56bdb56 优化账户设置缓存页面中的输入字段,添加持久提示和图标,提升用户体验 2025-05-30 08:27:10 +08:00
jxxghp
d450d02e18 在账户设置缓存页面中添加固定表头 2025-05-30 08:25:03 +08:00
jxxghp
85a766cc7b 调整多个组件的样式和结构,优化用户界面体验 2025-05-30 08:15:48 +08:00
jxxghp
a473f356c9 优化缓存管理页面 2025-05-29 22:56:40 +08:00
jxxghp
52b5fdf383 添加清空缓存确认提示,优化缓存管理页面的用户体验 2025-05-29 22:37:03 +08:00
jxxghp
b886f02043 缓存管理页面 2025-05-29 20:49:19 +08:00
jxxghp
61963ea497 reset 2025-05-29 20:12:14 +08:00
jxxghp
2f9b27ad9e reset 2025-05-29 20:11:34 +08:00
jxxghp
9334109767 Merge pull request #341 from madrays/v2
增加缓存管理页面
2025-05-29 12:32:51 +08:00
jxxghp
2bc52576d9 更新package.json中的版本号 2025-05-29 08:23:18 +08:00
jxxghp
700d2c4a51 刷新数据时重新加载文件夹配置,以确保插件正确显示。 2025-05-29 08:21:17 +08:00
madrays
103bdb32c8 增加缓存管理页面 2025-05-29 00:45:12 +08:00
jxxghp
92b745e180 优化搜索站点对话框 2025-05-28 21:25:37 +08:00
jxxghp
a2007083b8 更新MoviePilot自动更新设置逻辑,支持'release'和'dev'选项 2025-05-28 21:15:52 +08:00
jxxghp
36a5f7ff29 添加自动更新MoviePilot和站点资源的设置选项 2025-05-28 21:05:46 +08:00
jxxghp
f727aea51d 为多个设置组件的保存按钮添加图标,以提升用户体验和一致性。 2025-05-28 10:09:05 +08:00
jxxghp
936ca24328 优化对话框组件,添加图标以提升用户体验 2025-05-28 08:59:31 +08:00
jxxghp
62f49b6087 优化插件文件夹内插件的筛选逻辑 2025-05-28 08:49:53 +08:00
jxxghp
e9ddbf9962 添加代理服务器设置 2025-05-28 08:24:42 +08:00
jxxghp
196cf522e6 fix 2025-05-27 21:41:06 +08:00
jxxghp
3fce3bf4a7 优化多个组件的输入框,添加图标以提升用户体验,确保提示信息的一致性和可读性。 2025-05-27 21:38:25 +08:00
jxxghp
1cfee25695 优化多个组件的输入框,添加图标以提升用户体验,确保提示信息的一致性和可读性。 2025-05-27 21:23:08 +08:00
jxxghp
5711285a77 更新多个卡片组件,统一标题文本为“配置”,添加图标以提升用户体验,优化输入框提示信息,确保一致性和可读性。 2025-05-27 17:46:51 +08:00
jxxghp
e6f537ca3a 优化多个对话框组件的布局,添加图标以提升用户体验,调整部分文本提示,确保一致性和可读性。 2025-05-27 17:40:20 +08:00
jxxghp
3b5220af57 fix plugin list loading 2025-05-27 14:00:15 +08:00
jxxghp
fa6b4b1d2d 调整插件列表显示行数,从三行改为两行,以优化界面布局。 2025-05-27 13:49:55 +08:00
jxxghp
7968e5374b 优化文件夹内插件的显示顺序,确保按照保存顺序排列插件,提升用户体验。 2025-05-27 13:48:13 +08:00
jxxghp
64997ebe45 重构插件混合排序逻辑,优化全局排序配置,兼容旧格式,提升插件和文件夹的排序体验。 2025-05-27 13:40:55 +08:00
jxxghp
f8592b01e2 优化错误日志输出 2025-05-27 13:29:53 +08:00
jxxghp
087474f514 fix 2025-05-27 13:26:09 +08:00
jxxghp
1725088f05 fix 插件混合排序问题 2025-05-27 13:12:09 +08:00
jxxghp
ec1b756a3d 添加混合排序功能,重构插件列表显示逻辑,移除冗余代码并优化拖拽排序体验。 2025-05-27 13:01:08 +08:00
jxxghp
76a06e0817 移除 AddDownloadDialog 组件中的显示器宽度逻辑,简化对话框全屏显示设置 2025-05-27 07:54:34 +08:00
jxxghp
02fb608d7b 更新 PluginCard.vue 2025-05-26 22:40:48 +08:00
jxxghp
e17fc2fc12 更新 package.json 2025-05-26 21:38:10 +08:00
jxxghp
4f6c317652 修复 PersonDetailView 组件中的 VImg 标签,移除多余的 v-img 指令以简化代码。 2025-05-26 21:30:23 +08:00
jxxghp
46c198be26 重构 credits.vue 和 media.vue 组件,简化 API 路径处理,移除不必要的路由参数,同时优化 PersonCardListView 组件的样式。 2025-05-26 21:28:52 +08:00
jxxghp
8552203d43 PluginCard 组件中的实时日志弹窗代码 2025-05-26 13:26:13 +08:00
jxxghp
139eaa7016 优化 PluginCard 组件 2025-05-26 12:44:08 +08:00
jxxghp
d81120ab8f 为 PluginCard 组件添加实时日志弹窗功能 2025-05-26 12:37:49 +08:00
jxxghp
6353d56beb Merge pull request #339 from madrays/v2 2025-05-26 11:26:26 +08:00
madrays
aa05496b42 插件分身多语言支持 2025-05-26 11:20:10 +08:00
madrays
dc15e537d8 增加插件分身功能 2025-05-26 10:55:55 +08:00
58 changed files with 2263 additions and 472 deletions

View File

@@ -110,4 +110,4 @@
"i18n-ally.localesPaths": [
"src/locales"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.5.0",
"version": "2.5.2",
"private": true,
"type": "module",
"bin": "dist/service.js",

View File

@@ -1305,3 +1305,49 @@ export interface Workflow {
// 最后执行时间
last_time?: string
}
// 种子缓存项
export interface TorrentCacheItem {
// 种子hash用于操作标识
hash: string
// 站点域名
domain: string
// 种子标题
title: string
// 种子描述
description?: string
// 种子大小
size: number
// 发布时间
pubdate?: string
// 站点名称
site_name?: string
// 识别的媒体名称
media_name?: string
// 识别的媒体年份
media_year?: string
// 识别的媒体类型
media_type?: string
// 季集信息
season_episode?: string
// 资源信息
resource_term?: string
// 种子链接
enclosure?: string
// 详情页面
page_url?: string
// 海报图片
poster_path?: string
// 背景图片
backdrop_path?: string
}
// 种子缓存数据
export interface TorrentCacheData {
// 缓存数量
count: number
// 站点数量
sites: number
// 缓存数据
data: TorrentCacheItem[]
}

View File

@@ -117,7 +117,13 @@ function onClose() {
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard :title="t('customRule.title', { id: props.rule.id })">
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-filter-outline" class="me-2" />
</template>
<VCardTitle>{{ t('customRule.title', { id: props.rule.id }) }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn v-model="ruleInfoDialog" />
<VDivider />
<VCardText>
@@ -131,6 +137,7 @@ function onClose() {
:hint="t('customRule.hint.ruleId')"
persistent-hint
active
prepend-inner-icon="mdi-identifier"
/>
</VCol>
<VCol cols="12" md="6">
@@ -141,6 +148,7 @@ function onClose() {
:hint="t('customRule.hint.ruleName')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12">
@@ -151,6 +159,7 @@ function onClose() {
:hint="t('customRule.hint.include')"
persistent-hint
active
prepend-inner-icon="mdi-plus-circle"
/>
</VCol>
<VCol cols="12">
@@ -161,6 +170,7 @@ function onClose() {
:hint="t('customRule.hint.exclude')"
persistent-hint
active
prepend-inner-icon="mdi-minus-circle"
/>
</VCol>
<VCol cols="6">
@@ -171,6 +181,7 @@ function onClose() {
:hint="t('customRule.hint.sizeRange')"
persistent-hint
active
prepend-inner-icon="mdi-harddisk"
/>
</VCol>
<VCol cols="6">
@@ -181,6 +192,7 @@ function onClose() {
:hint="t('customRule.hint.seeders')"
persistent-hint
active
prepend-inner-icon="mdi-account-group"
/>
</VCol>
<VCol cols="6">
@@ -191,6 +203,7 @@ function onClose() {
:hint="t('customRule.hint.publishTime')"
persistent-hint
active
prepend-inner-icon="mdi-calendar-clock"
/>
</VCol>
</VRow>

View File

@@ -200,7 +200,14 @@ onUnmounted(() => {
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard :title="`${props.downloader.name} - ${t('downloader.title')}`">
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-download" class="me-2" />
</template>
<VCardTitle>{{ t('common.config') }}</VCardTitle>
<VCardSubtitle>{{ props.downloader.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn v-model="downloaderInfoDialog" />
<VDivider />
<VCardText>
@@ -226,6 +233,7 @@ onUnmounted(() => {
:hint="t('downloader.name')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -236,6 +244,7 @@ onUnmounted(() => {
:hint="t('downloader.host')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -245,6 +254,7 @@ onUnmounted(() => {
:hint="t('downloader.username')"
persistent-hint
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -255,6 +265,7 @@ onUnmounted(() => {
:hint="t('downloader.password')"
persistent-hint
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
@@ -303,6 +314,7 @@ onUnmounted(() => {
:hint="t('downloader.name')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -313,6 +325,7 @@ onUnmounted(() => {
:hint="t('downloader.host')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -322,6 +335,7 @@ onUnmounted(() => {
:hint="t('downloader.username')"
persistent-hint
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -332,6 +346,7 @@ onUnmounted(() => {
:hint="t('downloader.password')"
persistent-hint
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
</VRow>
@@ -343,6 +358,7 @@ onUnmounted(() => {
:hint="t('downloader.customTypeHint')"
persistent-hint
active
prepend-inner-icon="mdi-cog"
/>
</VCol>
<VCol cols="12" md="6">
@@ -352,6 +368,7 @@ onUnmounted(() => {
:hint="t('downloader.nameRequired')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
</VRow>

View File

@@ -243,6 +243,7 @@ function onClose() {
:hint="t('filterRule.groupName')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="6" md="3">
@@ -253,6 +254,7 @@ function onClose() {
:hint="t('filterRule.mediaType')"
persistent-hint
active
prepend-inner-icon="mdi-movie-open"
/>
</VCol>
<VCol cols="6" md="3">
@@ -263,6 +265,7 @@ function onClose() {
:hint="t('filterRule.category')"
persistent-hint
active
prepend-inner-icon="mdi-folder-open"
/>
</VCol>
</VRow>

View File

@@ -211,7 +211,14 @@ onMounted(() => {
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard :title="`${props.mediaserver.name} - ${t('common.config')}`">
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-cog" class="me-2" />
</template>
<VCardTitle>{{ t('common.config') }}</VCardTitle>
<VCardSubtitle>{{ props.mediaserver.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn v-model="mediaServerInfoDialog" />
<VDivider />
<VCardText>
@@ -230,6 +237,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -240,6 +248,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -250,6 +259,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -259,6 +269,7 @@ onMounted(() => {
:hint="t('mediaserver.embyApiKeyHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
@@ -273,6 +284,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -286,6 +298,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -296,6 +309,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -306,6 +320,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -315,6 +330,7 @@ onMounted(() => {
:hint="t('mediaserver.jellyfinApiKeyHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
@@ -329,6 +345,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -342,6 +359,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -352,6 +370,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12">
@@ -362,10 +381,16 @@ onMounted(() => {
: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 />
<VTextField
v-model="mediaServerInfo.config.username"
:label="t('mediaserver.username')"
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
@@ -373,6 +398,7 @@ onMounted(() => {
v-model="mediaServerInfo.config.password"
:label="t('mediaserver.password')"
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12">
@@ -387,6 +413,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -400,6 +427,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -410,6 +438,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -420,6 +449,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -429,6 +459,7 @@ onMounted(() => {
:hint="t('mediaserver.plexTokenHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
@@ -443,6 +474,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -454,10 +486,16 @@ onMounted(() => {
:label="t('mediaserver.type')"
:hint="t('mediaserver.customTypeHint')"
persistent-hint
prepend-inner-icon="mdi-cog"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField :label="t('common.name')" :hint="t('mediaserver.nameRequired')" persistent-hint />
<VTextField
:label="t('common.name')"
:hint="t('mediaserver.nameRequired')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
</VRow>
</VForm>

View File

@@ -148,8 +148,15 @@ function onClose() {
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard :title="`${props.notification.name} - ${t('notification.config')}`">
<VDialogCloseBtn v-model="notificationInfoDialog" />
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-cog" class="me-2" />
</template>
<VCardTitle>{{ t('common.config') }}</VCardTitle>
<VCardSubtitle>{{ props.notification.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="notificationInfoDialog = false" />
<VDivider />
<VCardText>
<VForm>
@@ -167,6 +174,7 @@ function onClose() {
clearable
chips
persistent-hint
prepend-inner-icon="mdi-bell-outline"
/>
</VCol>
</VRow>
@@ -178,6 +186,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -186,6 +195,7 @@ function onClose() {
:label="t('notification.wechat.corpId')"
:hint="t('notification.wechat.corpIdHint')"
persistent-hint
prepend-inner-icon="mdi-domain"
/>
</VCol>
<VCol cols="12" md="6">
@@ -194,6 +204,7 @@ function onClose() {
:label="t('notification.wechat.appId')"
:hint="t('notification.wechat.appIdHint')"
persistent-hint
prepend-inner-icon="mdi-application"
/>
</VCol>
<VCol cols="12" md="6">
@@ -202,6 +213,7 @@ function onClose() {
:label="t('notification.wechat.appSecret')"
:hint="t('notification.wechat.appSecretHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -210,6 +222,7 @@ function onClose() {
:label="t('notification.wechat.proxy')"
:hint="t('notification.wechat.proxyHint')"
persistent-hint
prepend-inner-icon="mdi-server-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -218,6 +231,7 @@ function onClose() {
:label="t('notification.wechat.token')"
:hint="t('notification.wechat.tokenHint')"
persistent-hint
prepend-inner-icon="mdi-key-variant"
/>
</VCol>
<VCol cols="12" md="6">
@@ -226,6 +240,7 @@ function onClose() {
:label="t('notification.wechat.encodingAesKey')"
:hint="t('notification.wechat.encodingAesKeyHint')"
persistent-hint
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
@@ -235,6 +250,7 @@ function onClose() {
:placeholder="t('notification.wechat.adminsPlaceholder')"
:hint="t('notification.wechat.adminsHint')"
persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/>
</VCol>
</VRow>
@@ -246,6 +262,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -254,6 +271,7 @@ function onClose() {
:label="t('notification.telegram.token')"
:hint="t('notification.telegram.tokenHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -262,6 +280,7 @@ function onClose() {
:label="t('notification.telegram.chatId')"
:hint="t('notification.telegram.chatIdHint')"
persistent-hint
prepend-inner-icon="mdi-chat"
/>
</VCol>
<VCol cols="12" md="6">
@@ -271,6 +290,7 @@ function onClose() {
:placeholder="t('notification.telegram.usersPlaceholder')"
:hint="t('notification.telegram.usersHint')"
persistent-hint
prepend-inner-icon="mdi-account-group"
/>
</VCol>
<VCol cols="12" md="6">
@@ -280,6 +300,7 @@ function onClose() {
:placeholder="t('notification.telegram.adminsPlaceholder')"
:hint="t('notification.telegram.adminsHint')"
persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/>
</VCol>
</VRow>
@@ -291,6 +312,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -300,6 +322,7 @@ function onClose() {
:placeholder="t('notification.slack.oauthTokenPlaceholder')"
:hint="t('notification.slack.oauthTokenHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -309,6 +332,7 @@ function onClose() {
:placeholder="t('notification.slack.appTokenPlaceholder')"
:hint="t('notification.slack.appTokenHint')"
persistent-hint
prepend-inner-icon="mdi-application"
/>
</VCol>
<VCol cols="12" md="6">
@@ -318,6 +342,7 @@ function onClose() {
:placeholder="t('notification.slack.channelPlaceholder')"
:hint="t('notification.slack.channelHint')"
persistent-hint
prepend-inner-icon="mdi-pound"
/>
</VCol>
</VRow>
@@ -329,6 +354,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -337,6 +363,7 @@ function onClose() {
:label="t('notification.synologychat.webhook')"
:hint="t('notification.synologychat.webhookHint')"
persistent-hint
prepend-inner-icon="mdi-webhook"
/>
</VCol>
<VCol cols="12" md="6">
@@ -345,6 +372,7 @@ function onClose() {
:label="t('notification.synologychat.token')"
:hint="t('notification.synologychat.tokenHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
</VRow>
@@ -356,6 +384,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -364,6 +393,7 @@ function onClose() {
:label="t('notification.vocechat.host')"
:hint="t('notification.vocechat.hostHint')"
persistent-hint
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -372,6 +402,7 @@ function onClose() {
:label="t('notification.vocechat.apiKey')"
:hint="t('notification.vocechat.apiKeyHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -381,6 +412,7 @@ function onClose() {
:placeholder="t('notification.vocechat.channelIdPlaceholder')"
:hint="t('notification.vocechat.channelIdHint')"
persistent-hint
prepend-inner-icon="mdi-pound"
/>
</VCol>
</VRow>
@@ -392,6 +424,7 @@ function onClose() {
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -400,6 +433,7 @@ function onClose() {
:label="t('notification.webpush.username')"
:hint="t('notification.webpush.usernameHint')"
persistent-hint
prepend-inner-icon="mdi-account"
/>
</VCol>
</VRow>
@@ -411,6 +445,7 @@ function onClose() {
:hint="t('notification.customTypeHint')"
persistent-hint
active
prepend-inner-icon="mdi-cog"
/>
</VCol>
<VCol cols="12" md="6">
@@ -419,7 +454,7 @@ function onClose() {
:label="t('notification.name')"
:hint="t('notification.nameRequired')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
</VRow>

View File

@@ -229,7 +229,7 @@ const dropdownItems = ref([
class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10"
>
<div class="flex flex-nowrap items-center w-full pe-10">
<div class="flex flex-nowrap max-w-32 items-center align-middle">
<div class="flex flex-nowrap max-w-40 items-center align-middle">
<VIcon icon="mdi-github" class="me-1" />
<a
class="overflow-hidden text-ellipsis whitespace-nowrap"

View File

@@ -10,6 +10,7 @@ import VersionHistory from '@/components/misc/VersionHistory.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
import PluginConfigDialog from '../dialog/PluginConfigDialog.vue'
import PluginDataDialog from '../dialog/PluginDataDialog.vue'
import LoggingView from '@/views/system/LoggingView.vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
@@ -58,6 +59,9 @@ const progressDialog = ref(false)
// 插件数据页面
const pluginInfoDialog = ref(false)
// 实时日志弹窗
const loggingDialog = ref(false)
// 进度框文本
const progressText = ref('正在更新插件...')
@@ -73,6 +77,18 @@ const imageLoadError = ref(false)
// 更新日志弹窗
const releaseDialog = ref(false)
// 插件分身对话框
const pluginCloneDialog = ref(false)
// 插件分身表单
const cloneForm = ref({
suffix: '',
name: '',
description: '',
version: '',
icon: '',
})
// 监听动作标识如为true则打开详情
watch(
() => props.action,
@@ -124,7 +140,12 @@ async function uninstallPlugin() {
// 通知父组件刷新
emit('remove')
} else {
$toast.error(t('plugin.uninstallFailed', { name: props.plugin?.plugin_name, message: result.message }))
$toast.error(
t('plugin.uninstallFailed', {
name: props.plugin?.plugin_name,
message: result.message,
}),
)
}
} catch (error) {
console.error(error)
@@ -178,7 +199,12 @@ async function resetPlugin() {
// 通知父组件刷新
emit('save')
} else {
$toast.error(t('plugin.resetFailed', { name: props.plugin?.plugin_name, message: result.message }))
$toast.error(
t('plugin.resetFailed', {
name: props.plugin?.plugin_name,
message: result.message,
}),
)
}
} catch (error) {
console.error(error)
@@ -209,7 +235,12 @@ async function updatePlugin() {
// 通知父组件刷新
emit('save')
} else {
$toast.error(t('plugin.updateFailed', { name: props.plugin?.plugin_name, message: result.message }))
$toast.error(
t('plugin.updateFailed', {
name: props.plugin?.plugin_name,
message: result.message,
}),
)
}
} catch (error) {
console.error(error)
@@ -241,6 +272,54 @@ function configDone() {
emit('save')
}
// 显示插件分身对话框
function showPluginClone() {
cloneForm.value = {
suffix: '',
name: t('plugin.cloneDefaultName', { name: props.plugin?.plugin_name }),
description: t('plugin.cloneDefaultDescription', { description: props.plugin?.plugin_desc }),
version: props.plugin?.plugin_version || '1.0',
icon: props.plugin?.plugin_icon || '',
}
pluginCloneDialog.value = true
}
// 执行插件分身
async function executePluginClone() {
if (!cloneForm.value.suffix.trim()) {
$toast.error(t('plugin.suffixRequired'))
return
}
try {
progressDialog.value = true
progressText.value = t('plugin.cloning', { name: props.plugin?.plugin_name })
const result: { [key: string]: any } = await api.post(`plugin/clone/${props.plugin?.id}`, {
suffix: cloneForm.value.suffix.trim(),
name: cloneForm.value.name.trim(),
description: cloneForm.value.description.trim(),
version: cloneForm.value.version.trim(),
icon: cloneForm.value.icon.trim(),
})
progressDialog.value = false
if (result.success) {
$toast.success(t('plugin.cloneSuccess', { name: cloneForm.value.name }))
pluginCloneDialog.value = false
// 通知父组件刷新
emit('remove')
} else {
$toast.error(t('plugin.cloneFailed', { message: result.message }))
}
} catch (error) {
progressDialog.value = false
$toast.error(t('plugin.cloneFailedGeneral'))
console.error(error)
}
}
// 弹出菜单
const dropdownItems = ref([
{
@@ -261,6 +340,16 @@ const dropdownItems = ref([
click: showPluginConfig,
},
},
{
title: t('plugin.clone'),
value: 8,
show: true,
props: {
prependIcon: 'mdi-content-copy',
color: 'info',
click: showPluginClone,
},
},
{
title: t('plugin.update'),
value: 3,
@@ -298,7 +387,7 @@ const dropdownItems = ref([
props: {
prependIcon: 'mdi-file-document-outline',
click: () => {
openLoggerWindow()
loggingDialog.value = true
},
},
},
@@ -366,7 +455,7 @@ watch(
{{ props.plugin?.plugin_desc }}
</div>
</div>
<div class="relative flex-shrink-0 self-center cursor-move pb-3">
<div class="relative flex-shrink-0 self-center pb-3" :class="{ 'cursor-move': display.mdAndUp.value }">
<VAvatar size="48">
<VImg
ref="imageRef"
@@ -384,7 +473,7 @@ watch(
class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10"
>
<div class="flex flex-nowrap items-center w-full pe-10">
<div class="flex flex-nowrap max-w-32 items-center align-middle">
<div class="flex flex-nowrap max-w-40 items-center align-middle">
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
<VIcon v-if="!isAvatarLoaded" size="small" icon="mdi-github" class="me-1" />
</VImg>
@@ -470,6 +559,144 @@ watch(
</VCardItem>
</VCard>
</VDialog>
<!-- 实时日志弹窗 -->
<VDialog
v-if="loggingDialog"
v-model="loggingDialog"
scrollable
max-width="60rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VDialogCloseBtn @click="loggingDialog = false" />
<VCardItem>
<VCardTitle class="d-inline-flex">
<VIcon icon="mdi-file-document" class="me-2" />
{{ t('plugin.logTitle') }}
<a class="mx-2 d-inline-flex align-center cursor-pointer" @click="openLoggerWindow">
<VChip color="grey-darken-1" size="small" class="ml-2">
<VIcon icon="mdi-open-in-new" size="small" start />
{{ t('common.openInNewWindow') }}
</VChip>
</a>
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<LoggingView :logfile="`plugins/${props.plugin?.id?.toLowerCase()}.log`" />
</VCardText>
</VCard>
</VDialog>
<!-- 插件分身对话框 -->
<VDialog
v-if="pluginCloneDialog"
v-model="pluginCloneDialog"
width="600"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-content-copy" class="me-2" />
</template>
<VCardTitle>{{ t('plugin.cloneTitle') }}</VCardTitle>
<VCardSubtitle>{{ t('plugin.cloneSubtitle', { name: props.plugin?.plugin_name }) }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="pluginCloneDialog = false" />
<VDivider />
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.suffix"
:label="t('plugin.suffix') + ' *'"
:placeholder="t('plugin.suffixPlaceholder')"
:hint="t('plugin.suffixHint')"
persistent-hint
:rules="[
v => !!v || t('plugin.suffixRequired'),
v => /^[a-zA-Z0-9]+$/.test(v) || t('plugin.suffixFormatError'),
v => v.length <= 20 || t('plugin.suffixLengthError'),
]"
required
prepend-inner-icon="mdi-tag"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.name"
:label="t('plugin.cloneName')"
:placeholder="t('plugin.cloneNamePlaceholder')"
:hint="t('plugin.cloneNameHint')"
persistent-hint
prepend-inner-icon="mdi-rename-box"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="cloneForm.description"
:label="t('plugin.cloneDescriptionLabel')"
:placeholder="t('plugin.cloneDescriptionPlaceholder')"
:hint="t('plugin.cloneDescriptionHint')"
persistent-hint
prepend-inner-icon="mdi-text"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.version"
:label="t('plugin.cloneVersion')"
:placeholder="t('plugin.cloneVersionPlaceholder')"
:hint="t('plugin.cloneVersionHint')"
persistent-hint
prepend-inner-icon="mdi-numeric"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.icon"
:label="t('plugin.cloneIcon')"
:placeholder="t('plugin.cloneIconPlaceholder')"
:hint="t('plugin.cloneIconHint')"
persistent-hint
prepend-inner-icon="mdi-image"
/>
</VCol>
<!-- 重要提醒 -->
<VCol cols="12">
<VAlert type="warning" variant="tonal" density="compact" class="mt-2" icon="mdi-alert-circle-outline">
<div class="text-body-2">
<strong>{{ t('common.notice') }}</strong
>{{ t('plugin.cloneNotice') }}
</div>
</VAlert>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn
color="primary"
@click="executePluginClone"
prepend-icon="mdi-content-copy"
class="px-5"
:disabled="!cloneForm.suffix.trim()"
>
{{ t('plugin.createClone') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</div>
</template>

View File

@@ -298,13 +298,18 @@ const dropdownItems = ref([
<div class="plugin-folder-card__body" :class="{ 'plugin-folder-card__body--no-icon': !shouldShowIcon }">
<!-- 文件夹图标 -->
<div v-if="shouldShowIcon" class="plugin-folder-card__icon-container">
<VIcon :icon="folderIcon" :size="display.mobile ? 56 : 72" class="cursor-move" :color="iconColor" />
<VIcon
:icon="folderIcon"
:size="display.mobile ? 56 : 72"
:color="iconColor"
:class="{ 'cursor-move': display.mdAndUp.value }"
/>
</div>
<!-- 文件夹信息 -->
<div
class="plugin-folder-card__info cursor-move"
:class="{ 'plugin-folder-card__info--no-icon': !shouldShowIcon }"
class="plugin-folder-card__info"
:class="{ 'cursor-move': display.mdAndUp.value, 'plugin-folder-card__info--no-icon': !shouldShowIcon }"
>
<!-- 文件夹名称 -->
<h3 class="plugin-folder-card__name">
@@ -347,10 +352,13 @@ const dropdownItems = ref([
<!-- 重命名对话框 -->
<VDialog v-if="renameDialog" v-model="renameDialog" max-width="400">
<VCard>
<VDialogCloseBtn @click="renameDialog = false" />
<VCardItem>
<template #prepend>
<VIcon icon="mdi-pencil" class="me-2" />
</template>
<VCardTitle>{{ t('folder.renameFolder') }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="renameDialog = false" />
<VDivider />
<VCardText>
<VTextField
@@ -462,6 +470,7 @@ const dropdownItems = ref([
variant="outlined"
:hint="t('folder.customBackgroundImageHint')"
persistent-hint
prepend-inner-icon="mdi-image"
/>
</VCol>
</VRow>

View File

@@ -0,0 +1,183 @@
<script lang="ts" setup>
import PluginCard from './PluginCard.vue'
import PluginFolderCard from './PluginFolderCard.vue'
interface MixedSortItem {
type: 'folder' | 'plugin'
id: string
data: any
order: number
}
interface Props {
item: MixedSortItem
pluginStatistics?: { [key: string]: number }
pluginActions?: { [key: string]: boolean }
showRemoveButton?: boolean
}
const props = withDefaults(defineProps<Props>(), {
pluginStatistics: () => ({}),
pluginActions: () => ({}),
showRemoveButton: false,
})
const emit = defineEmits<{
openFolder: [folderName: string]
deleteFolder: [folderName: string]
renameFolder: [oldName: string, newName: string]
updateFolderConfig: [folderName: string, config: any]
refreshData: []
actionDone: [pluginId: string]
removeFromFolder: [pluginId: string]
dropToFolder: [event: DragEvent, folderName: string]
}>()
// 拖拽事件处理
function handleDragOver(event: DragEvent) {
// 只有当拖拽的是插件时才允许放入文件夹
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
event.dataTransfer!.dropEffect = 'move'
const target = event.currentTarget as HTMLElement
target.classList.add('drag-over')
}
}
function handleDragEnter(event: DragEvent) {
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
}
}
function handleDragLeave(event: DragEvent) {
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
const target = event.currentTarget as HTMLElement
target.classList.remove('drag-over')
}
}
function handleDropToFolder(event: DragEvent) {
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
const target = event.currentTarget as HTMLElement
target.classList.remove('drag-over')
emit('dropToFolder', event, props.item.id)
}
}
</script>
<template>
<div class="mixed-sort-card-wrapper h-full">
<!-- 文件夹卡片 -->
<div
v-if="item.type === 'folder'"
class="drop-zone h-full"
:data-plugin-id="item.id"
@dragover="handleDragOver"
@dragenter="handleDragEnter"
@dragleave="handleDragLeave"
@drop="handleDropToFolder"
>
<PluginFolderCard
:folder-name="item.data.name"
:plugin-count="item.data.pluginCount"
:folder-config="item.data.config"
@open="$emit('openFolder', item.id)"
@delete="$emit('deleteFolder', item.id)"
@rename="(oldName, newName) => $emit('renameFolder', oldName, newName)"
@update-config="(folderName, config) => $emit('updateFolderConfig', folderName, config)"
/>
</div>
<!-- 插件卡片 -->
<div v-else-if="item.type === 'plugin'" class="plugin-item-wrapper h-full" :data-plugin-id="item.id">
<PluginCard
:count="pluginStatistics[item.id] || 0"
:plugin="item.data"
:action="pluginActions[item.id] || false"
@remove="$emit('refreshData')"
@save="$emit('refreshData')"
@action-done="$emit('actionDone', item.id)"
/>
<!-- 移出文件夹按钮(仅在文件夹内显示) -->
<VBtn
v-if="showRemoveButton"
icon="mdi-folder-remove"
variant="text"
color="warning"
size="small"
class="remove-from-folder-btn"
@click="$emit('removeFromFolder', item.id)"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.mixed-sort-card-wrapper {
block-size: 100%;
inline-size: 100%;
// 确保拖拽时的边界清晰
&.sortable-chosen {
opacity: 0.5;
}
&.sortable-ghost {
border: 2px dashed #2196f3;
border-radius: 16px;
background: rgba(33, 150, 243, 10%);
opacity: 0.3;
}
}
// 拖拽相关样式
.drop-zone {
position: relative;
isolation: isolate; // 创建新的层叠上下文
transition: all 0.3s ease;
&.drag-over {
border: 2px dashed #2196f3;
border-radius: 16px;
box-shadow: 0 0 20px rgba(33, 150, 243, 50%);
transform: scale(1.02);
}
}
.plugin-item-wrapper {
position: relative;
isolation: isolate; // 创建新的层叠上下文
.remove-from-folder-btn {
position: absolute;
z-index: 10;
border-radius: 50%;
backdrop-filter: blur(4px);
background: rgba(255, 255, 255, 10%);
inset-block-start: 4px;
inset-inline-end: 4px;
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover .remove-from-folder-btn {
opacity: 1;
}
}
// 拖拽时的样式优化
.mixed-sort-card-wrapper.sortable-drag {
.remove-from-folder-btn {
display: none !important;
}
}
</style>

View File

@@ -213,6 +213,9 @@ function onClose() {
>
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-cog" />
</template>
<VCardTitle>{{ t('storage.custom') }}</VCardTitle>
<VDialogCloseBtn v-model="customConfigDialog" />
</VCardItem>
@@ -225,11 +228,16 @@ function onClose() {
:label="t('storage.type')"
:hint="t('storage.customTypeHint')"
persistent-hint
active
prepend-inner-icon="mdi-database"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="customName" :label="t('storage.name')" persistent-hint active />
<VTextField
v-model="customName"
:label="t('storage.name')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
</VRow>
</VCardText>

View File

@@ -353,7 +353,7 @@ function onSubscribeEditRemove() {
<div>
<VCardText class="flex items-center pt-3 pb-2">
<div
class="h-auto w-14 flex-shrink-0 overflow-hidden rounded-md"
class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md"
v-if="imageLoaded"
:class="{ 'cursor-move': display.mdAndUp.value }"
>
@@ -367,7 +367,7 @@ function onSubscribeEditRemove() {
</div>
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
<div class="mr-2 min-w-0 text-lg font-bold text-white">
<div class="mr-2 min-w-0 text-lg font-bold text-white text-ellipsis overflow-hidden line-clamp-2 ...">
{{ props.media?.name }}
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
</div>

View File

@@ -179,7 +179,13 @@ const resolveProgress = (item: Workflow) => {
:loading="loading"
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }"
>
<VCardItem class="py-3" :class="`bg-${resolveStatusVariant(workflow?.state).color}`">
<VCardItem
:class="{
'py-1': workflow?.description,
'py-3': !workflow?.description,
[`bg-${resolveStatusVariant(workflow?.state).color}`]: true,
}"
>
<template #prepend>
<VAvatar variant="text" class="me-2">
<VIcon

View File

@@ -6,10 +6,6 @@ import type { DownloaderConf, MediaInfo, TorrentInfo, TransferDirectoryConf } fr
import { formatFileSize } from '@/@core/utils/formatters'
import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -136,71 +132,77 @@ onMounted(() => {
})
</script>
<template>
<VDialog max-width="35rem" scrollable :fullscreen="!display.mdAndUp.value">
<VDialog max-width="35rem" scrollable>
<VCard>
<VCardTitle class="py-4 me-12">
<VIcon icon="mdi-download" class="me-2" />
<span v-if="title">{{ torrent?.site_name }} - {{ title }}</span>
<span v-else>{{ t('dialog.addDownload.confirmDownload') }}</span>
</VCardTitle>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-monitor-arrow-down-variant" class="me-2" />
</template>
<VCardTitle>{{ t('dialog.addDownload.confirmDownload') }}</VCardTitle>
<VCardSubtitle>{{ torrent?.site_name }} - {{ title }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VList lines="one">
<VListItem>
<template #prepend>
<VIcon icon="mdi-web"></VIcon>
</template>
<VListItemTitle>
<span class="whitespace-break-spaces me-2">{{ torrent?.title }}</span>
<span class="text-green-700 ms-2 text-sm">{{ torrent?.seeders }}</span>
<span class="text-orange-700 ms-2 text-sm">{{ torrent?.peers }}</span>
</VListItemTitle>
</VListItem>
<VListItem v-if="torrent?.description">
<template #prepend>
<VIcon icon="mdi-subtitles-outline"></VIcon>
</template>
<VListItemTitle>
<span class="text-body-2 whitespace-break-spaces">{{ torrent?.description }}</span>
</VListItemTitle>
</VListItem>
<VListItem v-if="torrent?.size">
<template #prepend>
<VIcon icon="mdi-database"></VIcon>
</template>
<VListItemTitle>
<span class="text-body-2">
<VChip variant="tonal" label>
{{ formatFileSize(torrent?.size || 0) }}
</VChip>
</span>
</VListItemTitle>
</VListItem>
</VList>
<VRow class="px-7">
<VCol cols="12" md="4">
<VSelect
v-model="selectedDownloader"
:items="downloaderOptions"
size="small"
:label="t('dialog.addDownload.downloader')"
variant="underlined"
:placeholder="t('dialog.addDownload.defaultPlaceholder')"
density="compact"
/>
</VCol>
<VCol cols="12" md="8">
<VCombobox
v-model="selectedDirectory"
:items="targetDirectories"
:label="t('dialog.addDownload.saveDirectory')"
size="small"
:placeholder="t('dialog.addDownload.autoPlaceholder')"
variant="underlined"
density="compact"
/>
</VCol>
</VRow>
<VCardText>
<VList lines="one">
<VListItem>
<template #prepend>
<VIcon icon="mdi-web"></VIcon>
</template>
<VListItemTitle>
<span class="whitespace-break-spaces me-2">{{ torrent?.title }}</span>
<span class="text-green-700 ms-2 text-sm">{{ torrent?.seeders }}</span>
<span class="text-orange-700 ms-2 text-sm">{{ torrent?.peers }}</span>
</VListItemTitle>
</VListItem>
<VListItem v-if="torrent?.description">
<template #prepend>
<VIcon icon="mdi-subtitles-outline"></VIcon>
</template>
<VListItemTitle>
<span class="text-body-2 whitespace-break-spaces">{{ torrent?.description }}</span>
</VListItemTitle>
</VListItem>
<VListItem v-if="torrent?.size">
<template #prepend>
<VIcon icon="mdi-database"></VIcon>
</template>
<VListItemTitle>
<span class="text-body-2">
<VChip variant="tonal" label>
{{ formatFileSize(torrent?.size || 0) }}
</VChip>
</span>
</VListItemTitle>
</VListItem>
</VList>
<VRow class="px-5">
<VCol cols="12" md="6">
<VSelect
v-model="selectedDownloader"
:items="downloaderOptions"
size="small"
:label="t('dialog.addDownload.downloader')"
variant="underlined"
:placeholder="t('dialog.addDownload.defaultPlaceholder')"
density="comfortable"
prepend-inner-icon="mdi-download"
/>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="selectedDirectory"
:items="targetDirectories"
:label="t('dialog.addDownload.saveDirectory')"
size="small"
:placeholder="t('dialog.addDownload.autoPlaceholder')"
variant="underlined"
density="comfortable"
prepend-inner-icon="mdi-folder"
/>
</VCol>
</VRow>
</VCardText>
<VCardText class="text-center">
<VBtn variant="elevated" :disabled="loading" @click="addDownload" :prepend-icon="icon" class="px-5">
{{ buttonText }}

View File

@@ -74,6 +74,9 @@ async function savaAlistConfig() {
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardItem>
<template #prepend>
<VIcon icon="mdi-cog-outline" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.alistConfig.title') }}
</VCardTitle>
@@ -87,6 +90,7 @@ async function savaAlistConfig() {
:hint="t('dialog.alistConfig.serverUrl')"
:label="t('dialog.alistConfig.serverUrl')"
persistent-hint
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="4">
@@ -96,6 +100,7 @@ async function savaAlistConfig() {
:label="t('dialog.alistConfig.loginType')"
:hint="t('dialog.alistConfig.loginType')"
persistent-hint
prepend-inner-icon="mdi-login"
/>
</VCol>
<VCol cols="12" md="4" v-if="loginType == 'username'">
@@ -104,6 +109,7 @@ async function savaAlistConfig() {
:hint="t('dialog.alistConfig.username')"
:label="t('dialog.alistConfig.username')"
persistent-hint
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="4" v-if="loginType == 'username'">
@@ -113,6 +119,7 @@ async function savaAlistConfig() {
:hint="t('dialog.alistConfig.password')"
:label="t('dialog.alistConfig.password')"
persistent-hint
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="8" v-if="loginType == 'token'">
@@ -121,6 +128,7 @@ async function savaAlistConfig() {
:hint="t('dialog.alistConfig.loginTypeOptions.token')"
:label="t('dialog.alistConfig.loginTypeOptions.token')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
</VRow>

View File

@@ -114,6 +114,9 @@ onUnmounted(() => {
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardItem>
<template #prepend>
<VIcon icon="mdi-qrcode" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.aliyunAuth.loginTitle') }}
</VCardTitle>

View File

@@ -25,10 +25,16 @@ function handleImport() {
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard :title="props.title">
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-code-json" class="me-2" />
</template>
<VCardTitle>{{ props.title }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VTextarea v-model="codeString" />
<VTextarea v-model="codeString" prepend-inner-icon="mdi-code-json" />
</VCardText>
<VCardActions>
<VSpacer />

View File

@@ -61,6 +61,9 @@ async function handleReset() {
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardItem>
<template #prepend>
<VIcon icon="mdi-cog-outline" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.rcloneConfig.title') }}
</VCardTitle>
@@ -69,7 +72,11 @@ async function handleReset() {
<VCardText>
<VRow>
<VCol cols="12">
<VTextField v-model="props.conf.filepath" :label="t('dialog.rcloneConfig.filePath')" />
<VTextField
v-model="props.conf.filepath"
:label="t('dialog.rcloneConfig.filePath')"
prepend-inner-icon="mdi-file-document"
/>
</VCol>
<VCol cols="12">
<VAceEditor

View File

@@ -82,15 +82,18 @@ const storageOptions = computed(() => {
// 标题
const dialogTitle = computed(() => {
return t('dialog.reorganize.manualTitle')
})
// 副标题
const dialogSubtitle = computed(() => {
if (props.items) {
if (props.items.length > 1) return t('dialog.reorganize.multipleItemsTitle', { count: props.items.length })
return t('dialog.reorganize.singleItemTitle', { path: props.items[0].path })
} else if (props.logids) {
return t('dialog.reorganize.multipleItemsTitle', { count: props.logids.length })
}
return t('dialog.reorganize.manualTitle')
})
// 禁用指定集数
const disableEpisodeDetail = computed(() => {
if (props.items) {
@@ -250,7 +253,12 @@ onUnmounted(() => {
<template>
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="dialogTitle">
<VCard>
<VCardItem class="py-2">
<template #prepend> <VIcon icon="mdi-folder-move" class="me-2" /> </template>
<VCardTitle>{{ dialogTitle }}</VCardTitle>
<VCardSubtitle>{{ dialogSubtitle }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
@@ -264,6 +272,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.targetPathPlaceholder')"
:hint="t('dialog.reorganize.targetStorageHint')"
persistent-hint
prepend-inner-icon="mdi-harddisk"
/>
</VCol>
<VCol cols="12" md="6">
@@ -273,6 +282,7 @@ onUnmounted(() => {
:items="transferTypeOptions"
:hint="t('dialog.reorganize.transferTypeHint')"
persistent-hint
prepend-inner-icon="mdi-swap-horizontal"
>
<template v-slot:selection="{ item }">
{{ transferForm.transfer_type === '' ? t('dialog.reorganize.auto') : item.title }}
@@ -287,6 +297,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.targetPathPlaceholder')"
:hint="t('dialog.reorganize.targetPathHint')"
persistent-hint
prepend-inner-icon="mdi-folder-outline"
/>
</VCol>
</VRow>
@@ -302,6 +313,7 @@ onUnmounted(() => {
]"
:hint="t('dialog.reorganize.mediaTypeHint')"
persistent-hint
prepend-inner-icon="mdi-movie-open"
/>
</VCol>
<VCol cols="12" md="6">
@@ -315,6 +327,7 @@ onUnmounted(() => {
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
prepend-inner-icon="mdi-identifier"
@click:append-inner="mediaSelectorDialog = true"
/>
<VTextField
@@ -327,6 +340,7 @@ onUnmounted(() => {
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
prepend-inner-icon="mdi-identifier"
@click:append-inner="mediaSelectorDialog = true"
/>
</VCol>
@@ -339,6 +353,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeGroupPlaceholder')"
:hint="t('dialog.reorganize.episodeGroupHint')"
persistent-hint
prepend-inner-icon="mdi-view-list"
/>
</VCol>
<VCol cols="12" md="3">
@@ -348,6 +363,7 @@ onUnmounted(() => {
:items="seasonItems"
:hint="t('dialog.reorganize.seasonHint')"
persistent-hint
prepend-inner-icon="mdi-calendar"
/>
</VCol>
<VCol cols="12" md="3">
@@ -358,6 +374,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeDetailPlaceholder')"
:hint="t('dialog.reorganize.episodeDetailHint')"
persistent-hint
prepend-inner-icon="mdi-playlist-play"
/>
</VCol>
<VCol cols="12" md="6">
@@ -367,6 +384,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeFormatPlaceholder')"
:hint="t('dialog.reorganize.episodeFormatHint')"
persistent-hint
prepend-inner-icon="mdi-format-text"
/>
</VCol>
<VCol cols="12" md="6">
@@ -376,6 +394,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeOffsetPlaceholder')"
:hint="t('dialog.reorganize.episodeOffsetHint')"
persistent-hint
prepend-inner-icon="mdi-numeric"
/>
</VCol>
</VRow>
@@ -387,6 +406,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodePartPlaceholder')"
:hint="t('dialog.reorganize.episodePartHint')"
persistent-hint
prepend-inner-icon="mdi-file-multiple"
/>
</VCol>
<VCol cols="12" md="6">
@@ -397,6 +417,7 @@ onUnmounted(() => {
placeholder="0"
:hint="t('dialog.reorganize.minFileSizeHint')"
persistent-hint
prepend-inner-icon="mdi-file-document-outline"
/>
</VCol>
</VRow>

View File

@@ -58,23 +58,16 @@ const filteredSites = computed(() => {
<!-- Site Selection Dialog -->
<VDialog max-width="40rem" fullscreen-mobile>
<VCard class="site-dialog">
<VCardTitle class="d-flex align-center pa-4">
<span class="text-h6 font-weight-medium">{{ t('dialog.searchSite.selectSites') }}</span>
<VSpacer />
<VTextField
v-model="siteFilter"
:placeholder="t('dialog.searchSite.siteSearch')"
density="compact"
variant="outlined"
hide-details
class="ml-4"
style="max-inline-size: 200px"
prepend-inner-icon="mdi-magnify"
clearable
/>
</VCardTitle>
<VDivider class="search-divider" />
<VCardItem>
<template #prepend>
<VIcon icon="mdi-web-check" />
</template>
<VCardTitle>
{{ t('dialog.searchSite.selectSites') }}
</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText style="max-block-size: 420px" class="overflow-y-auto px-4 py-4">
<!-- 站点列表 -->
<div v-if="filteredSites.length > 0">
@@ -164,9 +157,6 @@ const filteredSites = computed(() => {
</VCardText>
<VCardActions class="pt-3">
<VBtn color="secondary" @click="emit('close')" class="mr-2 d-flex align-center justify-center">
{{ t('dialog.searchSite.cancel') }}
</VBtn>
<VSpacer />
<VBtn
color="primary"

View File

@@ -148,11 +148,14 @@ onMounted(async () => {
<template>
<VDialog scrollable :close-on-back="false" eager max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.oper === 'add' ? t('site.actions.add') : t('site.actions.edit')}${t('site.title')}${
props.oper !== 'add' ? ` - ${siteForm.name}` : ''
}`"
>
<VCard>
<VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
<template #prepend>
<VIcon :icon="oper == 'add' ? 'mdi-web-plus' : 'mdi-web'" class="me-2" />
</template>
<VCardTitle>{{ `${props.oper === 'add' ? t('site.actions.add') : t('site.actions.edit')}` }}</VCardTitle>
<VCardSubtitle>{{ siteForm.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
@@ -165,6 +168,7 @@ onMounted(async () => {
:rules="[requiredValidator]"
:hint="t('site.hints.url')"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
<VCol cols="6" md="3">
@@ -175,6 +179,7 @@ onMounted(async () => {
:rules="[requiredValidator]"
:hint="t('site.hints.priority')"
persistent-hint
prepend-inner-icon="mdi-priority-high"
/>
</VCol>
<VCol cols="6" md="3">
@@ -184,6 +189,7 @@ onMounted(async () => {
:label="t('site.fields.status')"
:hint="t('site.hints.status')"
persistent-hint
prepend-inner-icon="mdi-toggle-switch"
/>
</VCol>
</VRow>
@@ -194,6 +200,7 @@ onMounted(async () => {
:label="t('site.fields.rss')"
:hint="t('site.hints.rss')"
persistent-hint
prepend-inner-icon="mdi-rss"
/>
</VCol>
<VCol cols="12" md="3">
@@ -202,6 +209,7 @@ onMounted(async () => {
:label="t('site.fields.timeout')"
:hint="t('site.hints.timeout')"
persistent-hint
prepend-inner-icon="mdi-timer"
/>
</VCol>
<VCol cols="6" md="3">
@@ -211,6 +219,7 @@ onMounted(async () => {
:items="downloaderOptions"
:hint="t('site.hints.downloader')"
persistent-hint
prepend-inner-icon="mdi-download"
/>
</VCol>
</VRow>
@@ -237,6 +246,7 @@ onMounted(async () => {
:label="t('site.fields.cookie')"
:hint="t('site.hints.cookie')"
persistent-hint
prepend-inner-icon="mdi-cookie"
/>
</VCol>
<VCol cols="12">
@@ -245,6 +255,7 @@ onMounted(async () => {
:label="t('site.fields.userAgent')"
:hint="t('site.hints.userAgent')"
persistent-hint
prepend-inner-icon="mdi-web-box"
/>
</VCol>
</VRow>
@@ -257,6 +268,7 @@ onMounted(async () => {
:label="t('site.fields.authorization')"
:hint="t('site.hints.authorization')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -265,6 +277,7 @@ onMounted(async () => {
:label="t('site.fields.apiKey')"
:hint="t('site.hints.apiKey')"
persistent-hint
prepend-inner-icon="mdi-api"
/>
</VCol>
</VRow>
@@ -283,6 +296,7 @@ onMounted(async () => {
:rules="[numberValidator]"
:hint="t('site.hints.limitInterval')"
persistent-hint
prepend-inner-icon="mdi-clock-outline"
/>
</VCol>
<VCol cols="12" md="4">
@@ -292,6 +306,7 @@ onMounted(async () => {
:rules="[numberValidator]"
:hint="t('site.hints.limitCount')"
persistent-hint
prepend-inner-icon="mdi-counter"
/>
</VCol>
<VCol cols="12" md="4">
@@ -301,6 +316,7 @@ onMounted(async () => {
:rules="[numberValidator]"
:hint="t('site.hints.limitSeconds')"
persistent-hint
prepend-inner-icon="mdi-timer-sand"
/>
</VCol>
</VRow>

View File

@@ -153,6 +153,7 @@ onMounted(() => {
density="compact"
:label="t('dialog.siteResource.searchKeyword')"
clearable
prepend-inner-icon="mdi-magnify"
/>
</VCol>
<VCol cols="6" md="5">
@@ -165,10 +166,13 @@ onMounted(() => {
:label="t('dialog.siteResource.resourceCategory')"
multiple
clearable
prepend-inner-icon="mdi-folder"
/>
</VCol>
<VCol cols="12" md="2" class="text-center">
<VBtn block prepend-icon="mdi-magnify" @click="getResourceList">{{ t('dialog.siteResource.search') }}</VBtn>
<VBtn variant="tonal" block prepend-icon="mdi-magnify" @click="getResourceList">
{{ t('dialog.siteResource.search') }}
</VBtn>
</VCol>
</VRow>
</div>

View File

@@ -281,18 +281,24 @@ onMounted(() => {
<template>
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="
props.default
? t('dialog.subscribeEdit.titleDefault')
: t('dialog.subscribeEdit.titleEditFormat', {
name: subscribeForm.name,
season: subscribeForm.season
? t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season })
: '',
})
"
>
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-clipboard-list-outline" class="me-2" />
</template>
<VCardTitle>
{{ props.default ? t('dialog.subscribeEdit.titleDefault') : t('dialog.subscribeEdit.titleEdit') }}
</VCardTitle>
<VCardSubtitle v-if="!props.default">
{{ subscribeForm.name }}
<span v-if="subscribeForm.season">
{{ t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season }) }}
</span>
</VCardSubtitle>
<VCardSubtitle v-else>
{{ props.type }}
</VCardSubtitle>
</VCardItem>
<VCardText>
<VDialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}">
@@ -314,6 +320,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.searchKeyword')"
:hint="t('dialog.subscribeEdit.searchKeywordHint')"
persistent-hint
prepend-inner-icon="mdi-magnify"
/>
</VCol>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
@@ -323,6 +330,7 @@ onMounted(() => {
:rules="[numberValidator]"
:hint="t('dialog.subscribeEdit.totalEpisodeHint')"
persistent-hint
prepend-inner-icon="mdi-playlist-play"
/>
</VCol>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
@@ -332,6 +340,7 @@ onMounted(() => {
:rules="[numberValidator]"
:hint="t('dialog.subscribeEdit.startEpisodeHint')"
persistent-hint
prepend-inner-icon="mdi-play-circle-outline"
/>
</VCol>
</VRow>
@@ -343,6 +352,7 @@ onMounted(() => {
:items="qualityOptions"
:hint="t('dialog.subscribeEdit.qualityHint')"
persistent-hint
prepend-inner-icon="mdi-quality-high"
/>
</VCol>
<VCol cols="12" md="4">
@@ -352,6 +362,7 @@ onMounted(() => {
:items="resolutionOptions"
:hint="t('dialog.subscribeEdit.resolutionHint')"
persistent-hint
prepend-inner-icon="mdi-monitor"
/>
</VCol>
<VCol cols="12" md="4">
@@ -361,6 +372,7 @@ onMounted(() => {
:items="effectOptions"
:hint="t('dialog.subscribeEdit.effectHint')"
persistent-hint
prepend-inner-icon="mdi-auto-fix"
/>
</VCol>
</VRow>
@@ -375,6 +387,7 @@ onMounted(() => {
clearable
:hint="t('dialog.subscribeEdit.subscribeSitesHint')"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
</VRow>
@@ -386,6 +399,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.downloader')"
:hint="t('dialog.subscribeEdit.downloaderHint')"
persistent-hint
prepend-inner-icon="mdi-download"
/>
</VCol>
<VCol cols="12" md="6">
@@ -395,6 +409,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.savePath')"
:hint="t('dialog.subscribeEdit.savePathHint')"
persistent-hint
prepend-inner-icon="mdi-folder"
/>
</VCol>
</VRow>
@@ -435,6 +450,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.include')"
:hint="t('dialog.subscribeEdit.includeHint')"
persistent-hint
prepend-inner-icon="mdi-plus-circle-outline"
/>
</VCol>
<VCol cols="12" md="6">
@@ -443,6 +459,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.exclude')"
:hint="t('dialog.subscribeEdit.excludeHint')"
persistent-hint
prepend-inner-icon="mdi-minus-circle-outline"
/>
</VCol>
</VRow>
@@ -457,6 +474,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.filterGroups')"
:hint="t('dialog.subscribeEdit.filterGroupsHint')"
persistent-hint
prepend-inner-icon="mdi-filter"
/>
</VCol>
<VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6">
@@ -467,6 +485,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.episodeGroup')"
:hint="t('dialog.subscribeEdit.episodeGroupHint')"
persistent-hint
prepend-inner-icon="mdi-view-list"
/>
</VCol>
<VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6">
@@ -476,6 +495,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.season')"
:hint="t('dialog.subscribeEdit.seasonHint')"
persistent-hint
prepend-inner-icon="mdi-calendar"
/>
</VCol>
<VCol cols="12" v-if="!props.default">
@@ -484,6 +504,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.mediaCategory')"
:hint="t('dialog.subscribeEdit.mediaCategoryHint')"
persistent-hint
prepend-inner-icon="mdi-tag"
/>
</VCol>
</VRow>
@@ -495,6 +516,7 @@ onMounted(() => {
:hint="t('dialog.subscribeEdit.customWordsHint')"
persistent-hint
:placeholder="t('dialog.subscribeEdit.customWordsPlaceholder')"
prepend-inner-icon="mdi-text"
/>
</VCol>
</VRow>

View File

@@ -56,11 +56,18 @@ const $toast = useToast()
<template>
<VDialog scrollable max-width="30rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${t('dialog.subscribeShare.shareSubscription')} - ${props.sub?.name} ${
props.sub?.season ? t('dialog.subscribeShare.season', { number: props.sub?.season }) : ''
}`"
>
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-share-outline" class="me-2" />
</template>
<VCardTitle>{{ t('dialog.subscribeShare.shareSubscription') }}</VCardTitle>
<VCardSubtitle>
{{ props.sub?.name }}
{{ props.sub?.season ? t('dialog.subscribeShare.season', { number: props.sub?.season }) : '' }}
</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText>
<VDialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}" class="pt-2">
@@ -72,6 +79,7 @@ const $toast = useToast()
:label="t('dialog.subscribeShare.title')"
:rules="[requiredValidator]"
persistent-hint
prepend-inner-icon="mdi-format-title"
/>
</VCol>
<VCol cols="12">
@@ -81,6 +89,7 @@ const $toast = useToast()
:rules="[requiredValidator]"
:hint="t('dialog.subscribeShare.descriptionHint')"
persistent-hint
prepend-inner-icon="mdi-comment-text-outline"
/>
</VCol>
<VCol cols="12">
@@ -90,6 +99,7 @@ const $toast = useToast()
:rules="[requiredValidator]"
:hint="t('dialog.subscribeShare.shareUserHint')"
persistent-hint
prepend-inner-icon="mdi-account-outline"
/>
</VCol>
</VRow>

View File

@@ -119,6 +119,9 @@ onUnmounted(() => {
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardItem>
<template #prepend>
<VIcon icon="mdi-qrcode" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.u115Auth.loginTitle') }}
</VCardTitle>

View File

@@ -291,11 +291,14 @@ onMounted(() => {
<template>
<VDialog scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.oper === 'add' ? t('dialog.userAddEdit.add') : t('dialog.userAddEdit.edit')}${
props.oper !== 'add' ? ` - ${userName}` : ''
}`"
>
<VCard>
<VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
<template #prepend>
<VIcon icon="mdi-account" class="me-2" />
</template>
<VCardTitle>{{ props.oper === 'add' ? t('dialog.userAddEdit.add') : t('dialog.userAddEdit.edit') }}</VCardTitle>
<VCardSubtitle>{{ userName }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardItem>
@@ -350,6 +353,7 @@ onMounted(() => {
density="comfortable"
:readonly="props.oper !== 'add'"
:label="t('dialog.userAddEdit.username')"
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -359,6 +363,7 @@ onMounted(() => {
clearable
:label="t('dialog.userAddEdit.email')"
type="email"
prepend-inner-icon="mdi-email"
/>
</VCol>
<VCol cols="12" md="6">
@@ -370,6 +375,7 @@ onMounted(() => {
clearable
:label="t('dialog.userAddEdit.password')"
autocomplete=""
prepend-inner-icon="mdi-lock"
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
@@ -382,6 +388,7 @@ onMounted(() => {
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
clearable
:label="t('dialog.userAddEdit.confirmPassword')"
prepend-inner-icon="mdi-lock-check"
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
/>
</VCol>
@@ -392,6 +399,7 @@ onMounted(() => {
clearable
:label="t('dialog.userAddEdit.nickname')"
placeholder="显示昵称,优先于用户名显示"
prepend-inner-icon="mdi-card-account-details"
/>
</VCol>
<VCol cols="12" md="6" v-if="canControl">
@@ -402,6 +410,7 @@ onMounted(() => {
item-value="value"
:label="t('dialog.userAddEdit.status')"
dense
prepend-inner-icon="mdi-toggle-switch"
/>
</VCol>
</VRow>
@@ -415,6 +424,7 @@ onMounted(() => {
density="comfortable"
clearable
:label="t('dialog.userAddEdit.wechat')"
prepend-inner-icon="mdi-wechat"
/>
</VCol>
<VCol cols="12" md="6">
@@ -423,6 +433,7 @@ onMounted(() => {
density="comfortable"
clearable
:label="t('dialog.userAddEdit.telegram')"
prepend-inner-icon="mdi-send"
/>
</VCol>
<VCol cols="12" md="6">
@@ -431,6 +442,7 @@ onMounted(() => {
density="comfortable"
clearable
:label="t('dialog.userAddEdit.slack')"
prepend-inner-icon="mdi-slack"
/>
</VCol>
<VCol cols="12" md="6">
@@ -439,6 +451,7 @@ onMounted(() => {
density="comfortable"
clearable
:label="t('dialog.userAddEdit.vocechat')"
prepend-inner-icon="mdi-chat"
/>
</VCol>
<VCol cols="12" md="6">
@@ -447,10 +460,17 @@ onMounted(() => {
density="comfortable"
clearable
:label="t('dialog.userAddEdit.synologyChat')"
prepend-inner-icon="mdi-message"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="userForm.settings.douban_userid" density="comfortable" clearable label="豆瓣用户" />
<VTextField
v-model="userForm.settings.douban_userid"
density="comfortable"
clearable
label="豆瓣用户"
prepend-inner-icon="mdi-movie"
/>
</VCol>
</VRow>
</VForm>

View File

@@ -138,8 +138,15 @@ onMounted(async () => {
<template>
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard :title="t('dialog.userAuth.title')">
<VDialogCloseBtn @click="emit('close')" />
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-user-check" class="me-2" />
{{ t('dialog.userAuth.title') }}
</VCardTitle>
<VDialogCloseBtn @click="emit('close')" />
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
@@ -150,6 +157,7 @@ onMounted(async () => {
item-title="name"
:label="t('dialog.userAuth.selectSite')"
item-props
prepend-inner-icon="mdi-web"
>
</VSelect>
</VCol>

View File

@@ -86,7 +86,13 @@ async function editWorkflow() {
<template>
<VDialog scrollable :close-on-back="false" eager max-width="30rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="title">
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-clock-outline" class="me-2" />
</template>
<VCardTitle>{{ title }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
@@ -99,6 +105,7 @@ async function editWorkflow() {
:rules="[requiredValidator]"
persistent-hint
:hint="t('dialog.workflowAddEdit.namePlaceholder')"
prepend-inner-icon="mdi-workflow"
/>
</VCol>
<VCol cols="12">
@@ -109,6 +116,7 @@ async function editWorkflow() {
placeholder="5位cron表达式"
persistent-hint
:hint="t('dialog.workflowAddEdit.cronExprDesc')"
prepend-inner-icon="mdi-clock-outline"
/>
</VCol>
<VCol cols="12">
@@ -116,6 +124,7 @@ async function editWorkflow() {
v-model="workflowForm.description"
:label="t('dialog.workflowAddEdit.desc')"
:placeholder="t('dialog.workflowAddEdit.descPlaceholder')"
prepend-inner-icon="mdi-text-box-outline"
/>
</VCol>
</VRow>

View File

@@ -696,13 +696,24 @@ onMounted(() => {
</VCard>
<!-- 重命名弹窗 -->
<VDialog v-if="renamePopper" v-model="renamePopper" max-width="35rem">
<VCard :title="t('file.rename')">
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-pencil" class="me-2" />
</template>
<VCardTitle>{{ t('file.rename') }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="renamePopper = false" />
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VTextField v-model="newName" :label="t('file.newName')" :loading="renameLoading" />
<VTextField
v-model="newName"
:label="t('file.newName')"
:loading="renameLoading"
prepend-inner-icon="mdi-format-text"
/>
</VCol>
<VCol cols="12" v-if="currentItem && currentItem.type == 'dir'">
<VSwitch v-model="renameAll" :label="t('file.includeSubfolders')" />

View File

@@ -165,17 +165,24 @@ const sortIcon = computed(() => {
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
<VIcon icon="mdi-arrow-up-bold-outline" />
</IconBtn>
<!-- 新建文件夹 -->
<VDialog v-model="newFolderPopper" max-width="35rem">
<template #activator="{ props }">
<IconBtn>
<VIcon v-bind="props" icon="mdi-folder-plus-outline" />
</IconBtn>
</template>
<VCard :title="t('file.newFolder')">
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-folder-plus-outline" class="me-2" />
</template>
<VCardTitle>{{ t('file.newFolder') }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="newFolderPopper = false" />
<VDivider />
<VCardText>
<VTextField v-model="newFolderName" :label="t('common.name')" />
<VTextField v-model="newFolderName" :label="t('common.name')" prepend-inner-icon="mdi-format-text" />
</VCardText>
<VCardActions>
<div class="flex-grow-1" />

View File

@@ -270,7 +270,7 @@ onMounted(() => {
</VCardItem>
<VDivider />
<VCardText>
<LoggingView />
<LoggingView logfile="moviepilot.log" />
</VCardText>
</VCard>
</VDialog>

View File

@@ -39,6 +39,11 @@ export default {
unsubscribe: 'Unsubscribe',
media: 'Media',
unknown: 'Unknown',
notice: 'Notice',
itemsPerPage: 'Items per page',
pageText: '{0}-{1} of {2}',
noDataText: 'No data',
loadingText: 'Loading...',
},
mediaType: {
movie: 'Movie',
@@ -201,6 +206,10 @@ export default {
title: 'Services',
description: 'Scheduled jobs',
},
cache: {
title: 'Cache',
description: 'Torrent cache, media recognition data cache, image file cache management',
},
notification: {
title: 'Notifications',
description: 'Notification channels (WeChat, Telegram, Slack, SynologyChat, VoceChat, WebPush), message scope',
@@ -852,8 +861,8 @@ export default {
browserSimulation: 'Use browser simulation for authentic site access',
},
actions: {
add: 'Add',
edit: 'Edit',
add: 'Add Site',
edit: 'Edit Site',
},
messages: {
addSuccess: 'Site added successfully',
@@ -1095,10 +1104,16 @@ export default {
securityImageDomainsHint: 'Allowed image domains whitelist for caching, used to control trusted image sources',
noSecurityImageDomains: 'No security domains',
securityImageDomainAdd: 'Add domain, e.g.: image.tmdb.org',
proxyHost: 'Proxy Server',
proxyHostHint: 'Set proxy server address, support: http(s), socks5, socks5h, etc.',
moviePilotAutoUpdate: 'Auto Update MoviePilot',
moviePilotAutoUpdateHint: 'Automatically update MoviePilot to the latest release version when restarting',
autoUpdateResource: 'Auto Update Resource',
autoUpdateResourceHint: 'Automatically detect and update site resource package when restarting',
},
site: {
siteSync: 'Site Synchronization',
siteSyncDesc: 'Quickly sync site data from CookieCloud.',
siteSyncDesc: 'Quickly sync site data from CookieCloud',
enableLocalCookieCloud: 'Enable Local CookieCloud Server',
enableLocalCookieCloudHint:
'Use built-in CookieCloud service to sync site data, service address: http://localhost:3000/cookiecloud',
@@ -1144,7 +1159,7 @@ export default {
},
notification: {
channels: 'Notification Channels',
channelsDesc: 'Set message sending channel parameters.',
channelsDesc: 'Set message sending channel parameters',
organizeSuccess: 'Media Import',
downloadAdded: 'Download Added',
subscribeAdded: 'Subscribe Added',
@@ -1191,7 +1206,7 @@ export default {
},
words: {
customIdentifiers: 'Custom Identifiers',
identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification.',
identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification',
identifiersPlaceholder: 'Support regular expressions, special characters need \\ escape, one line for each rule',
identifiersHint: 'Support regular expressions, special characters need \\ escape, one line for each rule',
formatTitle: 'Supported configuration formats (mind the spaces):',
@@ -1231,7 +1246,7 @@ export default {
},
search: {
basicSettings: 'Basic Settings',
basicSettingsDesc: 'Set data sources, rule groups and other basic information.',
basicSettingsDesc: 'Set data sources, rule groups and other basic information',
recognizeSource: 'Recognition Data Source',
recognizeSourceDesc:
'Default is TMDB. Douban is usually more friendly for Chinese works, but some foreign works have incomplete information.',
@@ -1332,8 +1347,7 @@ export default {
},
scheduler: {
title: 'Scheduled Jobs',
subtitle:
"Includes built-in system services and plugin services. Manual execution will not affect the job's normal schedule.",
subtitle: 'Includes built-in system services and plugin services',
provider: 'Provider',
taskName: 'Task Name',
taskStatus: 'Task Status',
@@ -1380,6 +1394,55 @@ export default {
settingsSaveSuccess: 'Subscription basic settings saved successfully',
settingsSaveFailed: 'Failed to save subscription basic settings!',
},
cache: {
title: 'Cache Management',
subtitle: 'Manage torrent cache data',
filterByTitle: 'Filter by Title',
filterBySite: 'Filter by Site',
selectSite: 'Select Site',
refresh: 'Refresh Cache',
deleteSelected: 'Delete Selected',
clearAll: 'Clear All Cache',
refreshSuccess: 'Cache refresh completed',
refreshFailed: 'Failed to refresh cache',
clearSuccess: 'Cache clear completed',
clearFailed: 'Failed to clear cache',
deleteSuccess: 'Cache item deleted successfully',
deleteFailed: 'Failed to delete cache item',
deleteSelectedSuccess: 'Successfully deleted {count} cache items',
deleteSelectedFailed: 'Failed to delete cache items',
loadFailed: 'Failed to load cache data',
selectDeleteWarning: 'Please select cache items to delete',
reidentify: 'Re-identify',
reidentifySuccess: 'Re-identification completed',
reidentifyFailed: 'Re-identification failed',
poster: 'Poster',
torrentTitle: 'Title',
site: 'Site',
size: 'Size',
publishTime: 'Publish Time',
recognitionResult: 'Recognition Result',
actions: 'Actions',
unrecognized: 'Unrecognized',
noData: 'No cache data',
noDataHint: 'Click "Refresh Cache" button to get the latest torrent cache',
reidentifyDialog: {
title: 'Re-identify',
torrentInfo: 'Torrent Info',
tmdbId: 'TMDB ID',
tmdbIdHint: 'Optional, manually specify TMDB ID for recognition',
doubanId: 'Douban ID',
doubanIdHint: 'Optional, manually specify Douban ID for recognition',
autoHint: 'If no ID is specified, the torrent will be automatically re-identified',
cancel: 'Cancel',
confirm: 'Re-identify',
},
mediaType: {
movie: 'Movie',
tv: 'TV Show',
},
clearConfirm: 'Are you sure you want to clear all cache?',
},
},
dialog: {
progress: {
@@ -1703,8 +1766,8 @@ export default {
previous: 'Previous',
confirm: 'Confirm',
manualTitle: 'Manual Organization',
multipleItemsTitle: 'Organize - {count} Items',
singleItemTitle: 'Organize - {path}',
multipleItemsTitle: '{count} Items',
singleItemTitle: '{path}',
targetStorage: 'Target Storage',
targetStorageHint: 'Organization target storage',
transferType: 'Organization Method',
@@ -1753,7 +1816,7 @@ export default {
},
subscribeEdit: {
titleDefault: 'Default Subscription Rules',
titleEditFormat: 'Edit Subscription - {name} {season}',
titleEdit: 'Edit Subscription',
seasonFormat: 'Season {number}',
tabs: {
basic: 'Basic',
@@ -2013,6 +2076,40 @@ export default {
folderName: 'Folder Name',
cancel: 'Cancel',
create: 'Create',
clone: 'Clone',
cloneTitle: 'Create Plugin Clone',
cloneSubtitle: 'Create an independent clone instance for {name}',
cloneFeature: 'Plugin Clone Feature',
cloneDescription:
'Create an independent copy of the plugin with separate configuration and data, suitable for multi-account, testing environments, etc.',
suffix: 'Clone Suffix',
suffixPlaceholder: 'e.g.: Test, Backup, Site1',
suffixHint: 'Unique identifier to distinguish clones, only letters and numbers allowed',
suffixRequired: 'Clone suffix cannot be empty',
suffixFormatError: 'Only letters and numbers allowed',
suffixLengthError: 'Length cannot exceed 20 characters',
cloneName: 'Clone Name',
cloneNamePlaceholder: 'e.g.: Auto Backup Test Version',
cloneNameHint: 'Display name for the clone plugin (optional)',
cloneDefaultName: '{name} Clone',
cloneDescriptionLabel: 'Clone Description',
cloneDescriptionPlaceholder: 'Describe the purpose and features of this clone...',
cloneDescriptionHint: 'Detailed description of the clone plugin purpose (optional)',
cloneDefaultDescription: '{description} (Clone Version)',
cloneVersion: 'Version',
cloneVersionPlaceholder: 'e.g.: 1.0, 2.1.0',
cloneVersionHint: 'Custom version number for the clone plugin (optional)',
cloneIcon: 'Icon URL',
cloneIconPlaceholder: 'https://example.com/icon.png',
cloneIconHint: 'Custom icon for the clone plugin (optional)',
cloneNotice:
'Clone plugins are disabled by default after creation and need to be manually configured and enabled. The clone suffix cannot be modified once set.',
createClone: 'Create Clone',
cloning: 'Creating clone for {name}...',
cloneSuccess: 'Plugin clone {name} created successfully!',
cloneFailed: 'Plugin clone creation failed: {message}',
cloneFailedGeneral: 'Plugin clone creation failed',
logTitle: 'Plugin Logging',
},
profile: {
personalInfo: 'Personal Information',

View File

@@ -39,6 +39,11 @@ export default {
unsubscribe: '取消订阅',
media: '媒体',
unknown: '未知',
notice: '注意',
itemsPerPage: '每页条数',
pageText: '{0}-{1} 共 {2} 条',
noDataText: '没有数据',
loadingText: '加载中...',
},
mediaType: {
movie: '电影',
@@ -201,6 +206,10 @@ export default {
title: '服务',
description: '定时作业',
},
cache: {
title: '缓存',
description: '种子缓存、图片文件缓存管理',
},
notification: {
title: '通知',
description: '通知渠道微信、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息发送范围',
@@ -849,8 +858,8 @@ export default {
browserSimulation: '使用浏览器模拟真实访问该站点',
},
actions: {
add: '新增',
edit: '编辑',
add: '新增站点',
edit: '编辑站点',
},
messages: {
addSuccess: '新增站点成功',
@@ -936,7 +945,7 @@ export default {
system: {
custom: '自定义',
basicSettings: '基础设置',
basicSettingsDesc: '设置服务器的全局功能',
basicSettingsDesc: '设置服务器的全局功能',
appDomain: '访问域名',
appDomainHint: '用于发送通知时,添加快捷跳转地址',
wallpaper: '背景壁纸',
@@ -1085,10 +1094,16 @@ export default {
securityImageDomainsHint: '允许缓存的图片域名白名单,用于控制可信任的图片来源',
noSecurityImageDomains: '暂无安全域名',
securityImageDomainAdd: '添加域名image.tmdb.org',
proxyHost: '代理服务器',
proxyHostHint: '设置代理服务器地址支持http(s)、socks5、socks5h 等协议',
moviePilotAutoUpdate: '自动更新MoviePilot',
moviePilotAutoUpdateHint: '重启时自动更新MoviePilot到最新发行版本',
autoUpdateResource: '自动更新站点资源',
autoUpdateResourceHint: '重启时自动检测和更新站点资源包',
},
site: {
siteSync: '站点同步',
siteSyncDesc: '从CookieCloud快速同步站点数据',
siteSyncDesc: '从CookieCloud快速同步站点数据',
enableLocalCookieCloud: '启用本地CookieCloud服务器',
enableLocalCookieCloudHint: '使用内建CookieCloud服务同步站点数据服务地址为http://localhost:3000/cookiecloud',
serviceAddress: '服务地址',
@@ -1131,7 +1146,7 @@ export default {
},
notification: {
channels: '通知渠道',
channelsDesc: '设置消息发送渠道参数',
channelsDesc: '设置消息发送渠道参数',
organizeSuccess: '资源入库',
downloadAdded: '资源下载',
subscribeAdded: '添加订阅',
@@ -1178,7 +1193,7 @@ export default {
},
words: {
customIdentifiers: '自定义识别词',
identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别',
identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别',
identifiersPlaceholder: '支持正则表达式,特殊字符需要\\转义,一行为一组',
identifiersHint: '支持正则表达式,特殊字符需要\\转义,一行为一组',
formatTitle: '支持的配置格式(注意空格):',
@@ -1214,7 +1229,7 @@ export default {
},
search: {
basicSettings: '基础设置',
basicSettingsDesc: '设定数据源、规则组等基础信息',
basicSettingsDesc: '设定数据源、规则组等基础信息',
recognizeSource: '识别数据源',
recognizeSourceDesc: '默认使用TMDB。豆瓣识别中文作品通常更友好但有些国外作品信息不完整。',
themoviedb: 'TheMovieDb',
@@ -1250,7 +1265,7 @@ export default {
},
directory: {
storage: '存储',
storageDesc: '设置本地或网盘存储',
storageDesc: '设置本地或网盘存储',
directory: '目录',
mediaType: '媒体类型',
directoryDesc: '设置媒体文件整理目录结构,按先后顺序依次匹配。',
@@ -1312,7 +1327,7 @@ export default {
},
scheduler: {
title: '定时作业',
subtitle: '包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。',
subtitle: '包含系统内置服务以及插件提供的服务',
provider: '提供者',
taskName: '任务名称',
taskStatus: '任务状态',
@@ -1359,6 +1374,55 @@ export default {
settingsSaveSuccess: '订阅基础设置保存成功',
settingsSaveFailed: '订阅基础设置保存失败!',
},
cache: {
title: '缓存管理',
subtitle: '管理缓存的站点资源',
filterByTitle: '按标题筛选',
filterBySite: '按站点筛选',
selectSite: '选择站点',
refresh: '刷新缓存',
deleteSelected: '删除选中',
clearAll: '清空缓存',
refreshSuccess: '缓存刷新完成',
refreshFailed: '刷新缓存失败',
clearSuccess: '缓存清理完成',
clearFailed: '清理缓存失败',
deleteSuccess: '缓存项删除成功',
deleteFailed: '删除缓存项失败',
deleteSelectedSuccess: '成功删除 {count} 个缓存项',
deleteSelectedFailed: '删除缓存项失败',
loadFailed: '加载缓存数据失败',
selectDeleteWarning: '请选择要删除的缓存项',
reidentify: '重新识别',
reidentifySuccess: '重新识别完成',
reidentifyFailed: '重新识别失败',
poster: '海报',
torrentTitle: '标题',
site: '站点',
size: '大小',
publishTime: '发布时间',
recognitionResult: '识别结果',
actions: '操作',
unrecognized: '未识别',
noData: '暂无缓存数据',
noDataHint: '点击"刷新缓存"按钮获取最新的种子缓存',
reidentifyDialog: {
title: '重新识别',
torrentInfo: '种子信息',
tmdbId: 'TMDB ID',
tmdbIdHint: '可选手动指定TMDB ID进行识别',
doubanId: '豆瓣 ID',
doubanIdHint: '可选手动指定豆瓣ID进行识别',
autoHint: '如果不指定ID将自动重新识别该种子',
cancel: '取消',
confirm: '重新识别',
},
mediaType: {
movie: '电影',
tv: '电视剧',
},
clearConfirm: '确认清空所有缓存吗?',
},
},
dialog: {
progress: {
@@ -1680,8 +1744,8 @@ export default {
previous: '上一步',
confirm: '确认',
manualTitle: '手动整理',
multipleItemsTitle: '整理 - 共 {count} 项',
singleItemTitle: '整理 - {path}',
multipleItemsTitle: '共 {count} 项',
singleItemTitle: '{path}',
targetStorage: '目的存储',
targetStorageHint: '整理目的存储',
transferType: '整理方式',
@@ -1730,7 +1794,7 @@ export default {
},
subscribeEdit: {
titleDefault: '默认订阅规则',
titleEditFormat: '编辑订阅 - {name} {season}',
titleEdit: '编辑订阅',
seasonFormat: '第 {number} 季',
tabs: {
basic: '基础',
@@ -1989,6 +2053,38 @@ export default {
folderName: '文件夹名称',
cancel: '取消',
create: '创建',
clone: '分身',
cloneTitle: '创建插件分身',
cloneSubtitle: '为 {name} 创建独立的分身实例',
cloneFeature: '插件分身功能',
cloneDescription: '创建插件的独立副本,拥有独立的配置和数据,适用于多账号、测试环境等场景',
suffix: '分身后缀',
suffixPlaceholder: '例如Test、Backup、Site1',
suffixHint: '用于区分分身的唯一标识,只能包含英文字母和数字',
suffixRequired: '分身后缀不能为空',
suffixFormatError: '只能包含英文字母和数字',
suffixLengthError: '长度不能超过20个字符',
cloneName: '分身名称',
cloneNamePlaceholder: '例如:自动备份 测试版',
cloneNameHint: '分身插件的显示名称(可选)',
cloneDefaultName: '{name} 分身',
cloneDescriptionLabel: '分身描述',
cloneDescriptionPlaceholder: '描述这个分身的用途和特点...',
cloneDescriptionHint: '详细描述分身插件的用途(可选)',
cloneDefaultDescription: '{description} (分身版本)',
cloneVersion: '版本号',
cloneVersionPlaceholder: '例如1.0、2.1.0',
cloneVersionHint: '自定义分身插件的版本号(可选)',
cloneIcon: '图标URL',
cloneIconPlaceholder: 'https://example.com/icon.png',
cloneIconHint: '自定义分身插件的图标(可选)',
cloneNotice: '分身插件创建后默认为禁用状态,需要手动配置启用。分身后缀一旦确定无法修改。',
createClone: '创建分身',
cloning: '正在创建 {name} 的分身...',
cloneSuccess: '插件分身 {name} 创建成功!',
cloneFailed: '插件分身创建失败:{message}',
cloneFailedGeneral: '插件分身创建失败',
logTitle: '插件日志',
},
profile: {
personalInfo: '个人信息',

View File

@@ -39,6 +39,11 @@ export default {
unsubscribe: '取消訂閱',
media: '媒體',
unknown: '未知',
notice: '注意',
itemsPerPage: '每頁條數',
pageText: '{0}-{1} 共 {2} 條',
noDataText: '沒有數據',
loadingText: '加載中...',
},
mediaType: {
movie: '電影',
@@ -202,6 +207,10 @@ export default {
title: '服務',
description: '定時作業',
},
cache: {
title: '緩存',
description: '種子緩存、識別媒體數據緩存、圖片文件緩存管理',
},
notification: {
title: '通知',
description: '通知渠道微信、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息發送範圍',
@@ -851,8 +860,8 @@ export default {
browserSimulation: '使用瀏覽器模擬真實訪問該站點',
},
actions: {
add: '新增',
edit: '編輯',
add: '新增站點',
edit: '編輯站點',
},
messages: {
addSuccess: '新增站點成功',
@@ -938,7 +947,7 @@ export default {
system: {
custom: '自定義',
basicSettings: '基礎設置',
basicSettingsDesc: '設置服務器的全局功能',
basicSettingsDesc: '設置服務器的全局功能',
appDomain: '訪問域名',
appDomainHint: '用於發送通知時,添加快捷跳轉地址',
wallpaper: '背景壁紙',
@@ -1087,10 +1096,16 @@ export default {
securityImageDomainsHint: '允許緩存的圖片域名白名單,用於控制可信任的圖片來源',
noSecurityImageDomains: '暫無安全域名',
securityImageDomainAdd: '添加域名image.tmdb.org',
proxyHost: '代理服務器',
proxyHostHint: '設置代理服務器地址支持http(s)、socks5、socks5h 等協議',
moviePilotAutoUpdate: '自動更新MoviePilot',
moviePilotAutoUpdateHint: '重啟時自動更新MoviePilot到最新發行版本',
autoUpdateResource: '自動更新站點資源',
autoUpdateResourceHint: '重啟時自動檢測和更新站點資源包',
},
site: {
siteSync: '站點同步',
siteSyncDesc: '從CookieCloud快速同步站點數據',
siteSyncDesc: '從CookieCloud快速同步站點數據',
enableLocalCookieCloud: '啟用本地CookieCloud服務器',
enableLocalCookieCloudHint: '使用內建CookieCloud服務同步站點數據服務地址為http://localhost:3000/cookiecloud',
serviceAddress: '服務地址',
@@ -1133,7 +1148,7 @@ export default {
},
notification: {
channels: '通知渠道',
channelsDesc: '設置消息發送渠道參數',
channelsDesc: '設置消息發送渠道參數',
organizeSuccess: '資源入庫',
downloadAdded: '資源下載',
subscribeAdded: '添加訂閱',
@@ -1180,7 +1195,7 @@ export default {
},
words: {
customIdentifiers: '自定義識別詞',
identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別',
identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別',
identifiersPlaceholder: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
identifiersHint: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
formatTitle: '支持的配置格式(注意空格):',
@@ -1216,7 +1231,7 @@ export default {
},
search: {
basicSettings: '基礎設置',
basicSettingsDesc: '設定數據源、規則組等基礎信息',
basicSettingsDesc: '設定數據源、規則組等基礎信息',
recognizeSource: '識別數據源',
recognizeSourceDesc: '默認使用TMDB。豆瓣識別中文作品通常更友好但有些國外作品信息不完整。',
themoviedb: 'TheMovieDb',
@@ -1252,7 +1267,7 @@ export default {
},
directory: {
storage: '存儲',
storageDesc: '設置本地或網盤存儲',
storageDesc: '設置本地或網盤存儲',
directory: '目錄',
directoryDesc: '設置媒體文件整理目錄結構,按先後順序依次匹配。',
organizeAndScrap: '整理 & 刮削',
@@ -1313,7 +1328,7 @@ export default {
},
scheduler: {
scheduledTasks: '定時作業',
scheduledTasksDesc: '包含系統內置服務以及插件提供的服務,手動執行不會影響作業正常的時間表。',
scheduledTasksDesc: '包含系統內置服務以及插件提供的服務',
provider: '提供者',
taskName: '任務名稱',
taskStatus: '任務狀態',
@@ -1360,6 +1375,56 @@ export default {
settingsSaveSuccess: '訂閱基礎設置保存成功',
settingsSaveFailed: '訂閱基礎設置保存失敗!',
},
cache: {
title: '緩存',
description: '種子緩存、圖片文件緩存管理',
subtitle: '管理緩存的站點資源',
filterByTitle: '按標題篩選',
filterBySite: '按站點篩選',
selectSite: '選擇站點',
refresh: '刷新緩存',
deleteSelected: '刪除選中',
clearAll: '清空緩存',
refreshSuccess: '緩存刷新完成',
refreshFailed: '刷新緩存失敗',
clearSuccess: '緩存清理完成',
clearFailed: '清理緩存失敗',
deleteSuccess: '緩存項刪除成功',
deleteFailed: '刪除緩存項失敗',
deleteSelectedSuccess: '成功刪除 {count} 個緩存項',
deleteSelectedFailed: '刪除緩存項失敗',
loadFailed: '加載緩存數據失敗',
selectDeleteWarning: '請選擇要刪除的緩存項',
reidentify: '重新識別',
reidentifySuccess: '重新識別完成',
reidentifyFailed: '重新識別失敗',
poster: '海報',
torrentTitle: '標題',
site: '站點',
size: '大小',
publishTime: '發布時間',
recognitionResult: '識別結果',
actions: '操作',
unrecognized: '未識別',
noData: '暫無緩存數據',
noDataHint: '點擊"刷新緩存"按鈕獲取最新的種子緩存',
reidentifyDialog: {
title: '重新識別',
torrentInfo: '種子信息',
tmdbId: 'TMDB ID',
tmdbIdHint: '可選手動指定TMDB ID進行識別',
doubanId: '豆瓣 ID',
doubanIdHint: '可選手動指定豆瓣ID進行識別',
autoHint: '如果不指定ID將自動重新識別該種子',
cancel: '取消',
confirm: '重新識別',
},
mediaType: {
movie: '電影',
tv: '電視劇',
},
clearConfirm: '確認清空所有緩存嗎?',
},
},
dialog: {
progress: {
@@ -1681,8 +1746,8 @@ export default {
previous: '上一步',
confirm: '確認',
manualTitle: '手動整理',
multipleItemsTitle: '整理 - 共 {count} 項',
singleItemTitle: '整理 - {path}',
multipleItemsTitle: '共 {count} 項',
singleItemTitle: '{path}',
targetStorage: '目的存儲',
targetStorageHint: '整理目的存儲',
transferType: '整理方式',
@@ -1731,7 +1796,7 @@ export default {
},
subscribeEdit: {
titleDefault: '默認訂閱規則',
titleEditFormat: '編輯訂閱 - {name} {season}',
titleEdit: '編輯訂閱',
seasonFormat: '第 {number} 季',
tabs: {
basic: '基礎',
@@ -1990,6 +2055,38 @@ export default {
folderName: '文件夾名稱',
cancel: '取消',
create: '創建',
clone: '分身',
cloneTitle: '創建插件分身',
cloneSubtitle: '為 {name} 創建獨立的分身實例',
cloneFeature: '插件分身功能',
cloneDescription: '創建插件的獨立副本,擁有獨立的配置和數據,適用於多賬號、測試環境等場景',
suffix: '分身後綴',
suffixPlaceholder: '例如Test、Backup、Site1',
suffixHint: '用於區分分身的唯一標識,只能包含英文字母和數字',
suffixRequired: '分身後綴不能為空',
suffixFormatError: '只能包含英文字母和數字',
suffixLengthError: '長度不能超過20個字符',
cloneName: '分身名稱',
cloneNamePlaceholder: '例如:自動備份 測試版',
cloneNameHint: '分身插件的顯示名稱(可選)',
cloneDefaultName: '{name} 分身',
cloneDescriptionLabel: '分身描述',
cloneDescriptionPlaceholder: '描述這個分身的用途和特點...',
cloneDescriptionHint: '詳細描述分身插件的用途(可選)',
cloneDefaultDescription: '{description} (分身版本)',
cloneVersion: '版本號',
cloneVersionPlaceholder: '例如1.0、2.1.0',
cloneVersionHint: '自定義分身插件的版本號(可選)',
cloneIcon: '圖標URL',
cloneIconPlaceholder: 'https://example.com/icon.png',
cloneIconHint: '自定義分身插件的圖標(可選)',
cloneNotice: '分身插件創建後默認為禁用狀態,需要手動配置啟用。分身後綴一旦確定無法修改。',
createClone: '創建分身',
cloning: '正在創建 {name} 的分身...',
cloneSuccess: '插件分身 {name} 創建成功!',
cloneFailed: '插件分身創建失敗:{message}',
cloneFailedGeneral: '插件分身創建失敗',
logTitle: '插件日誌',
},
profile: {
personalInfo: '個人信息',

View File

@@ -1,24 +1,28 @@
<script setup lang="ts">
import PersonCardListView from '@/views/discover/PersonCardListView.vue'
// 输入参数
const props = defineProps({
// API路径
paths: Array as PropType<string[]> | PropType<string>,
})
// 路由参数
const route = useRoute()
const id = route.query?.id?.toString()
const title = route.query?.title?.toString()
const source = route.query?.source?.toString()
const type = route.query?.type?.toString()
const apipath = route.query?.apipath?.toString()
// 标题
let title = route.query?.title?.toString()
// 计算API路径
function getApiPath(paths: string[] | string) {
if (Array.isArray(paths)) return paths.join('/')
else return paths
}
</script>
<template>
<div>
<VPageContentTitle :title="title" />
<PersonCardListView
:credits-id="id"
:credits-name="title"
:credits-source="source"
:credits-type="type"
:credits-apipath="apipath"
/>
<PersonCardListView :apipath="getApiPath(props.paths || '')" />
</div>
</template>

View File

@@ -14,12 +14,6 @@ const mediaid = route.query?.mediaid?.toString()
// 类型:电影、电视剧
const type = route.query?.type?.toString()
// 媒体信息来源TMDB、豆瓣
const source = route.query?.source?.toString() || 'themoviedb'
// TMDB ID
const page = route.query?.page?.toString() || '1'
// 标题
const title = route.query?.title?.toString()
@@ -29,6 +23,6 @@ const year = route.query?.year?.toString()
<template>
<div>
<MediaDetailView :mediaid="mediaid" :type="type" :source="source" :page="page" :title="title" :year="year" />
<MediaDetailView :mediaid="mediaid" :type="type" :title="title" :year="year" />
</div>
</template>

View File

@@ -8,9 +8,10 @@ import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
import AccountSettingScheduler from '@/views/setting/AccountSettingScheduler.vue'
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import AccountSettingCache from '@/views/setting/AccountSettingCache.vue'
import { getSettingTabs } from '@/router/i18n-menu'
const route = useRoute()
@@ -81,7 +82,16 @@ const settingTabs = computed(() => getSettingTabs())
<VWindowItem value="scheduler">
<transition name="fade-slide" appear>
<div>
<AccountSettingScheduler />
<AccountSettingService />
</div>
</transition>
</VWindowItem>
<!-- 缓存 -->
<VWindowItem value="cache">
<transition name="fade-slide" appear>
<div>
<AccountSettingCache />
</div>
</transition>
</VWindowItem>

View File

@@ -170,6 +170,12 @@ export function getSettingTabs() {
tab: 'scheduler',
description: t('settingTabs.scheduler.description'),
},
{
title: t('settingTabs.cache.title'),
icon: 'mdi-database',
tab: 'cache',
description: t('settingTabs.cache.description'),
},
{
title: t('settingTabs.notification.title'),
icon: 'mdi-bell',

View File

@@ -113,7 +113,7 @@ async function fetchData({ done }: { done: any }) {
<template>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible" @load="fetchData">
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-3" @load="fetchData">
<template #loading />
<template #empty />
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card" tabindex="0">

View File

@@ -114,7 +114,7 @@ onBeforeMount(() => {
'ring-1 ring-gray-700': isImageLoaded,
}"
>
<VImg v-img :src="getPersonImage()" cover @load="isImageLoaded = true" />
<VImg :src="getPersonImage()" cover @load="isImageLoaded = true" />
</VAvatar>
<div class="ms-3">
<h1 class="text-3xl lg:text-4xl text-center text-lg-left">

View File

@@ -5,7 +5,6 @@ import api from '@/api'
import type { Plugin } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import PluginAppCard from '@/components/cards/PluginAppCard.vue'
import PluginCard from '@/components/cards/PluginCard.vue'
import noImage from '@images/logos/plugin.png'
import { useDisplay } from 'vuetify'
import { isNullOrEmptyObject } from '@/@core/utils'
@@ -13,7 +12,7 @@ import { getPluginTabs } from '@/router/i18n-menu'
import PluginMarketSettingDialog from '@/components/dialog/PluginMarketSettingDialog.vue'
import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n'
import PluginFolderCard from '@/components/cards/PluginFolderCard.vue'
import PluginMixedSortCard from '@/components/cards/PluginMixedSortCard.vue'
// 国际化
const { t } = useI18n()
@@ -39,7 +38,7 @@ const pluginId = ref(route.query.id)
const activeSort = ref(null)
// 插件顺序配置
const orderConfig = ref<{ id: string }[]>([])
const orderConfig = ref<{ id: string; type?: string; order?: number }[]>([])
// 排序选项
const sortOptions = computed(() => [
@@ -176,6 +175,33 @@ const newFolderDialog = ref(false)
// 新文件夹名称
const newFolderName = ref('')
// 获取文件夹内筛选后的插件
const getFilteredFolderPlugins = (folderName: string) => {
const folderData = pluginFolders.value[folderName]
const folderPluginIds = Array.isArray(folderData) ? folderData : folderData?.plugins || []
// 获取文件夹内的插件并应用筛选条件
const folderPlugins: Plugin[] = []
folderPluginIds.forEach((pluginId: string) => {
const plugin = dataList.value.find(p => p.id === pluginId)
if (plugin) {
folderPlugins.push(plugin)
}
})
// 应用筛选条件
return folderPlugins.filter(plugin => {
if (!installedFilter.value && !hasUpdateFilter.value) return true
if (hasUpdateFilter.value) {
return plugin.has_update
}
if (installedFilter.value) {
return plugin.plugin_name?.toLowerCase().includes((installedFilter.value as string).toLowerCase())
}
return true
})
}
// 显示的插件列表(考虑文件夹筛选)
const displayedPlugins = computed(() => {
if (!currentFolder.value) {
@@ -187,15 +213,21 @@ const displayedPlugins = computed(() => {
})
return filteredDataList.value.filter(plugin => !folderedPluginIds.has(plugin.id))
} else {
// 文件夹内:只显示文件夹中的插件
const folderData = pluginFolders.value[currentFolder.value]
const folderPluginIds = Array.isArray(folderData) ? folderData : folderData?.plugins || []
return filteredDataList.value.filter(plugin => folderPluginIds.includes(plugin.id))
// 文件夹内:返回筛选后的插件
return getFilteredFolderPlugins(currentFolder.value)
}
})
// 可拖拽的插件列表(主列表用)
const draggableMainPlugins = ref<Plugin[]>([])
// 混合排序项目类型
interface MixedSortItem {
type: 'folder' | 'plugin'
id: string
data: any
order: number
}
// 混合排序列表(包含文件夹和插件)
const mixedSortList = ref<MixedSortItem[]>([])
// 可拖拽的插件列表(文件夹内用)
const draggableFolderPlugins = ref<Plugin[]>([])
@@ -203,30 +235,6 @@ const draggableFolderPlugins = ref<Plugin[]>([])
// 是否正在拖拽排序中
const isDraggingSortMode = ref(false)
// 监听displayedPlugins变化更新可拖拽列表避免拖拽时的循环更新
watch(
displayedPlugins,
newPlugins => {
if (isDraggingSortMode.value) return // 拖拽排序时跳过更新
if (!currentFolder.value) {
draggableMainPlugins.value = [...newPlugins]
} else {
draggableFolderPlugins.value = [...newPlugins]
}
},
{ immediate: true },
)
// 监听文件夹切换,更新可拖拽列表
watch(currentFolder, () => {
if (!currentFolder.value) {
draggableMainPlugins.value = [...displayedPlugins.value]
} else {
draggableFolderPlugins.value = [...displayedPlugins.value]
}
})
// 显示的文件夹列表(按排序显示)
const displayedFolders = computed(() => {
if (currentFolder.value) return [] // 在文件夹内不显示其他文件夹
@@ -239,17 +247,108 @@ const displayedFolders = computed(() => {
const unsortedFolders = folderNames.filter(name => !folderOrder.value.includes(name))
sortedFolderNames.push(...unsortedFolders)
return sortedFolderNames.map(folderName => {
const folderData = pluginFolders.value[folderName]
const plugins = Array.isArray(folderData) ? folderData : folderData?.plugins || []
const config = Array.isArray(folderData) ? {} : folderData
return sortedFolderNames
.map(folderName => {
const folderData = pluginFolders.value[folderName]
const config = Array.isArray(folderData) ? {} : folderData
return {
name: folderName,
pluginCount: plugins.length,
config: config,
// 获取筛选后的插件数量
const filteredPlugins = getFilteredFolderPlugins(folderName)
return {
name: folderName,
pluginCount: filteredPlugins.length,
config: config,
}
})
.filter(folder => {
// 当有筛选条件时,只显示包含筛选后插件的文件夹
if (installedFilter.value || hasUpdateFilter.value) {
return folder.pluginCount > 0
}
return true
})
})
// 更新混合排序列表
function updateMixedSortList() {
if (isDraggingSortMode.value) return // 拖拽排序时跳过更新
if (!currentFolder.value) {
// 主列表:创建混合列表
const items: MixedSortItem[] = []
// 创建统一的排序索引
let globalOrder = 0
// 始终使用全局排序配置来创建混合列表
const allItems: { type: 'folder' | 'plugin'; id: string; data: any; order: number }[] = []
// 添加文件夹项目
displayedFolders.value.forEach(folder => {
const orderItem = orderConfig.value.find((item: any) => item.type === 'folder' && item.id === folder.name)
allItems.push({
type: 'folder',
id: folder.name,
data: folder,
order: orderItem?.order ?? 999,
})
})
// 添加插件项目
displayedPlugins.value.forEach(plugin => {
const orderItem = orderConfig.value.find((item: any) => item.type === 'plugin' && item.id === plugin.id)
allItems.push({
type: 'plugin',
id: plugin.id || '',
data: plugin,
order: orderItem?.order ?? 999,
})
})
// 按order排序
allItems.sort((a, b) => a.order - b.order)
// 转换为MixedSortItem格式
allItems.forEach((item, index) => {
items.push({
type: item.type,
id: item.id,
data: item.data,
order: index,
})
})
// 按order排序
items.sort((a, b) => a.order - b.order)
mixedSortList.value = items
} else {
// 文件夹内:只更新插件列表
draggableFolderPlugins.value = [...displayedPlugins.value]
}
}
// 监听相关数据变化,更新混合排序列表
watch(
[displayedPlugins, displayedFolders, orderConfig, folderOrder, installedFilter, hasUpdateFilter],
() => {
// 只有在非拖拽状态下才更新
if (!isDraggingSortMode.value) {
updateMixedSortList()
}
})
},
{
immediate: true,
deep: true,
},
)
// 监听文件夹切换,更新列表
watch(currentFolder, () => {
// 只有在非拖拽状态下才更新
if (!isDraggingSortMode.value) {
updateMixedSortList()
}
})
// 加载插件顺序
@@ -257,11 +356,33 @@ async function loadPluginOrderConfig() {
// 顺序配置
const local_order = localStorage.getItem('MP_PLUGIN_ORDER')
if (local_order) {
orderConfig.value = JSON.parse(local_order)
const parsed = JSON.parse(local_order)
// 兼容旧格式只有id和新格式包含type和order
if (parsed.length > 0 && typeof parsed[0] === 'object' && 'type' in parsed[0]) {
orderConfig.value = parsed
} else {
// 旧格式,转换为新格式
orderConfig.value = parsed.map((item: any, index: number) => ({
id: typeof item === 'string' ? item : item.id,
type: 'plugin',
order: index,
}))
}
} else {
const response2 = await api.get('/user/config/PluginOrder')
if (response2 && response2.data && response2.data.value) {
orderConfig.value = response2.data.value
const serverData = response2.data.value
// 兼容服务端的旧格式和新格式
if (serverData.length > 0 && typeof serverData[0] === 'object' && 'type' in serverData[0]) {
orderConfig.value = serverData
} else {
// 旧格式,转换为新格式
orderConfig.value = serverData.map((item: any, index: number) => ({
id: typeof item === 'string' ? item : item.id,
type: 'plugin',
order: index,
}))
}
localStorage.setItem('MP_PLUGIN_ORDER', JSON.stringify(orderConfig.value))
}
}
@@ -282,56 +403,75 @@ function sortPluginOrder() {
})
}
// 保存顺序设置
async function savePluginOrder() {
// 只在主列表中保存顺序,文件夹内不保存全局顺序
if (currentFolder.value) return
// 顺序配置
const orderObj = filteredDataList.value.map(item => ({ id: item.id || '' }))
orderConfig.value = orderObj
const orderString = JSON.stringify(orderObj)
localStorage.setItem('MP_PLUGIN_ORDER', orderString)
// 保存到服务端
// 保存混合排序
async function saveMixedSortOrder() {
try {
await api.post('/user/config/PluginOrder', orderObj)
} catch (error) {
console.error(error)
}
}
// 分离文件夹和插件,并记录它们的全局排序位置
const newFolderOrder: string[] = []
const newPluginOrder: Plugin[] = []
const globalOrder: { type: 'folder' | 'plugin'; id: string; order: number }[] = []
// 保存主列表插件顺序
async function saveMainPluginOrder() {
try {
// 更新主列表数据
const newOrderedList = [...draggableMainPlugins.value]
mixedSortList.value.forEach((item, index) => {
globalOrder.push({
type: item.type,
id: item.id,
order: index,
})
// 添加文件夹中的插件到末尾
if (item.type === 'folder') {
newFolderOrder.push(item.id)
} else if (item.type === 'plugin') {
newPluginOrder.push(item.data)
}
})
// 更新文件夹排序并设置order属性
folderOrder.value = newFolderOrder
newFolderOrder.forEach((folderName, index) => {
if (pluginFolders.value[folderName]) {
// 找到该文件夹在全局排序中的位置
const globalOrderItem = globalOrder.find(item => item.type === 'folder' && item.id === folderName)
pluginFolders.value[folderName].order = globalOrderItem ? globalOrderItem.order : index
}
})
// 添加文件夹中的插件到插件列表末尾
Object.values(pluginFolders.value).forEach(folderData => {
const plugins = Array.isArray(folderData) ? folderData : folderData.plugins || []
plugins.forEach((id: string) => {
const folderPlugin = dataList.value.find(p => p.id === id)
if (folderPlugin && !newOrderedList.find(p => p.id === id)) {
newOrderedList.push(folderPlugin)
if (folderPlugin && !newPluginOrder.find(p => p.id === id)) {
newPluginOrder.push(folderPlugin)
}
})
})
filteredDataList.value = newOrderedList
// 更新插件列表
filteredDataList.value = newPluginOrder
// 保存排序配置
const orderObj = newOrderedList.map(item => ({ id: item.id || '' }))
// 保存插件排序配置(包含全局排序信息)
const orderObj = globalOrder.map(item => ({
id: item.id,
type: item.type,
order: item.order,
}))
orderConfig.value = orderObj
const orderString = JSON.stringify(orderObj)
localStorage.setItem('MP_PLUGIN_ORDER', orderString)
// 保存到服务端
await api.post('/user/config/PluginOrder', orderObj)
// 保存文件夹排序
await savePluginFolders()
} catch (error) {
console.error(error)
} finally {
// 清除拖拽标志
isDraggingSortMode.value = false
// 在清除拖拽标志后更新混合排序列表显示
updateMixedSortList()
}
}
@@ -353,11 +493,36 @@ async function saveFolderPluginOrder() {
folderData.plugins = newPluginIds
}
// 更新全局排序配置中文件夹内插件的顺序
const folderOrderItem = orderConfig.value.find(
(item: any) => item.type === 'folder' && item.id === currentFolder.value,
)
const folderGlobalOrder = folderOrderItem?.order ?? 999
// 为文件夹内的插件分配连续的order值
newPluginIds.forEach((pluginId, index) => {
const existingItem = orderConfig.value.find((item: any) => item.type === 'plugin' && item.id === pluginId)
if (existingItem) {
existingItem.order = folderGlobalOrder + 0.1 + index * 0.01 // 使用小数确保在文件夹后面
} else {
orderConfig.value.push({
id: pluginId,
type: 'plugin',
order: folderGlobalOrder + 0.1 + index * 0.01,
})
}
})
// 保存全局排序配置
const orderString = JSON.stringify(orderConfig.value)
localStorage.setItem('MP_PLUGIN_ORDER', orderString)
await api.post('/user/config/PluginOrder', orderConfig.value)
// 保存到后端
await savePluginFolders()
}
} catch (error) {
console.error('保存文件夹内排序失败:', error)
console.error(error)
} finally {
// 清除拖拽标志
isDraggingSortMode.value = false
@@ -523,6 +688,8 @@ async function getPluginStatistics() {
async function refreshData() {
await fetchInstalledPlugins()
fetchUninstalledPlugins()
// 重新加载文件夹配置,确保分身插件能正确显示在文件夹中
await loadPluginFolders()
}
// 对uninstalledList进行排序到sortedUninstalledList
@@ -680,10 +847,18 @@ async function loadPluginFolders() {
pluginFolders.value = processedFolders
// 设置文件夹排序
folderOrder.value = Object.keys(processedFolders).sort(
(a, b) => (processedFolders[a].order || 0) - (processedFolders[b].order || 0),
)
// 设置文件夹排序 - 使用全局排序配置
const folderNames = Object.keys(processedFolders)
folderOrder.value = folderNames.sort((a, b) => {
// 从全局排序配置中查找文件夹的order
const aOrderItem = orderConfig.value.find((item: any) => item.type === 'folder' && item.id === a)
const bOrderItem = orderConfig.value.find((item: any) => item.type === 'folder' && item.id === b)
const aOrder = aOrderItem?.order ?? processedFolders[a].order ?? 999
const bOrder = bOrderItem?.order ?? processedFolders[b].order ?? 999
return aOrder - bOrder
})
} catch (error) {
pluginFolders.value = {}
folderOrder.value = []
@@ -791,7 +966,7 @@ async function renameFolder(oldName: string, newName: string) {
$toast.success(t('plugin.folderRenameSuccess'))
} catch (error) {
console.error('重命名文件夹失败:', error)
console.error(error)
// 回滚本地更改
pluginFolders.value[oldName] = pluginFolders.value[newName] || { plugins: [] }
delete pluginFolders.value[newName]
@@ -885,33 +1060,10 @@ async function updateFolderConfig(folderName: string, config: any) {
}
}
// 文件夹拖拽排序结束事件
function onFolderSortEnd() {
// 保存新的文件夹顺序
savePluginFolders()
}
// 当前拖拽的插件ID
const currentDraggedPluginId = ref('')
// 处理拖拽到文件夹的事件
function handleDragOver(event: DragEvent) {
event.preventDefault()
event.dataTransfer!.dropEffect = 'move'
const target = event.currentTarget as HTMLElement
target.classList.add('drag-over')
}
function handleDragEnter(event: DragEvent) {
event.preventDefault()
}
function handleDragLeave(event: DragEvent) {
event.preventDefault()
const target = event.currentTarget as HTMLElement
target.classList.remove('drag-over')
}
async function handleDropToFolder(event: DragEvent, folderName: string) {
event.preventDefault()
event.stopPropagation()
@@ -964,9 +1116,9 @@ async function handleDropToFolder(event: DragEvent, folderName: string) {
})
// 从主列表中移除(如果存在)
const mainIndex = draggableMainPlugins.value.findIndex(p => p.id === pluginId)
const mainIndex = mixedSortList.value.findIndex(item => item.type === 'plugin' && item.id === pluginId)
if (mainIndex > -1) {
draggableMainPlugins.value.splice(mainIndex, 1)
mixedSortList.value.splice(mainIndex, 1)
}
// 添加到目标文件夹
@@ -993,9 +1145,11 @@ async function handleDropToFolder(event: DragEvent, folderName: string) {
// 保存配置
await savePluginFolders()
// 更新混合排序列表
updateMixedSortList()
$toast.success(`插件已移动到文件夹 "${folderName}"`)
} catch (error) {
console.error('拖拽到文件夹失败:', error)
$toast.error('操作失败')
}
}
@@ -1008,10 +1162,18 @@ function onDragStartPlugin(evt: any) {
// 从oldIndex获取插件ID
const oldIndex = evt.oldIndex
if (oldIndex !== undefined) {
const plugin = currentFolder.value ? draggableFolderPlugins.value[oldIndex] : draggableMainPlugins.value[oldIndex]
if (plugin && plugin.id) {
currentDraggedPluginId.value = plugin.id
return
if (currentFolder.value) {
const plugin = draggableFolderPlugins.value[oldIndex]
if (plugin && plugin.id) {
currentDraggedPluginId.value = plugin.id
return
}
} else {
const item = mixedSortList.value[oldIndex]
if (item && item.id) {
currentDraggedPluginId.value = item.id
return
}
}
}
@@ -1034,13 +1196,6 @@ function onDragStartPlugin(evt: any) {
currentDraggedPluginId.value = item.getAttribute('data-plugin-id')
}
}
// 拖拽结束事件
function onDragEndPlugin(evt: any) {
currentDraggedPluginId.value = ''
// 清除拖拽标志
isDraggingSortMode.value = false
}
</script>
<template>
@@ -1207,66 +1362,37 @@ function onDragEndPlugin(evt: any) {
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<!-- 文件夹和插件网格 -->
<div v-if="displayedFolders.length > 0 || displayedPlugins.length > 0" class="grid gap-4 grid-plugin-card">
<!-- 文件夹卡片 - 使用draggable进行排序 -->
<draggable
v-if="displayedFolders.length > 0 && isRefreshed"
v-model="folderOrder"
@end="onFolderSortEnd"
handle=".cursor-move"
item-key="name"
tag="div"
:component-data="{ style: 'display: contents;' }"
:disabled="currentFolder !== ''"
group="folders"
>
<template #item="{ element: folderName }">
<div
v-if="displayedFolders.find(f => f.name === folderName)"
class="drop-zone"
@dragover="handleDragOver($event)"
@dragenter="handleDragEnter($event)"
@dragleave="handleDragLeave($event)"
@drop="handleDropToFolder($event, folderName)"
>
<PluginFolderCard
:folder-name="folderName"
:plugin-count="displayedFolders.find(f => f.name === folderName)?.pluginCount || 0"
:folder-config="displayedFolders.find(f => f.name === folderName)?.config || {}"
@open="openFolder"
@delete="deleteFolder"
@rename="renameFolder"
@update-config="updateFolderConfig"
/>
</div>
</template>
</draggable>
<!-- 插件卡片 -->
<div v-if="(mixedSortList.length > 0 || displayedPlugins.length > 0) && isRefreshed">
<!-- 混合排序列表文件夹和插件 -->
<template v-if="!currentFolder">
<!-- 主列表使用draggable进行排序 -->
<!-- 主列表使用draggable进行混合排序 -->
<draggable
v-model="draggableMainPlugins"
@end="saveMainPluginOrder"
v-model="mixedSortList"
@end="saveMixedSortOrder"
@start="onDragStartPlugin"
@sort="onDragEndPlugin"
handle=".cursor-move"
item-key="id"
tag="div"
:component-data="{ style: 'display: contents;' }"
group="plugins"
class="grid gap-4 grid-plugin-card"
group="mixed"
>
<template #item="{ element }">
<div class="plugin-item-wrapper" :data-plugin-id="element.id">
<PluginCard
:count="PluginStatistics[element.id || '0']"
:plugin="element"
:action="pluginActions[element.id || '0']"
@remove="refreshData"
@save="refreshData"
@action-done="pluginActions[element.id || '0'] = false"
/>
</div>
<PluginMixedSortCard
:item="element"
:plugin-statistics="PluginStatistics"
:plugin-actions="pluginActions"
@open-folder="openFolder"
@delete-folder="deleteFolder"
@rename-folder="(oldName, newName) => renameFolder(oldName, newName)"
@update-folder-config="(folderName, config) => updateFolderConfig(folderName, config)"
@refresh-data="refreshData"
@action-done="
pluginId => {
pluginActions[pluginId] = false
}
"
@drop-to-folder="(event, folderName) => handleDropToFolder(event, folderName)"
/>
</template>
</draggable>
</template>
@@ -1280,29 +1406,23 @@ function onDragEndPlugin(evt: any) {
handle=".cursor-move"
item-key="id"
tag="div"
:component-data="{ style: 'display: contents;' }"
class="grid gap-4 grid-plugin-card"
group="plugins"
>
<template #item="{ element }">
<div class="plugin-item-wrapper" :data-plugin-id="element.id">
<PluginCard
:count="PluginStatistics[element.id || '0']"
:plugin="element"
:action="pluginActions[element.id || '0']"
@remove="refreshData"
@save="refreshData"
@action-done="pluginActions[element.id || '0'] = false"
/>
<!-- 移出文件夹按钮 -->
<VBtn
icon="mdi-folder-remove"
variant="text"
color="warning"
size="small"
class="remove-from-folder-btn"
@click="removeFromFolder(element.id || '')"
/>
</div>
<PluginMixedSortCard
:item="{ type: 'plugin', id: element.id, data: element, order: 0 }"
:plugin-statistics="PluginStatistics"
:plugin-actions="pluginActions"
:show-remove-button="true"
@refresh-data="refreshData"
@action-done="
pluginId => {
pluginActions[pluginId] = false
}
"
@remove-from-folder="removeFromFolder"
/>
</template>
</draggable>
</template>
@@ -1402,7 +1522,7 @@ function onDragEndPlugin(evt: any) {
/>
</VToolbar>
<VDialogCloseBtn @click="closeSearchDialog" />
<VList v-if="filterPlugins.length > 0" lines="three">
<VList v-if="filterPlugins.length > 0" lines="two">
<VVirtualScroll :items="filterPlugins">
<template #default="{ item }">
<VListItem @click="openPlugin(item)">
@@ -1440,6 +1560,7 @@ function onDragEndPlugin(evt: any) {
</VList>
</VCard>
</VDialog>
<!-- 安装插件进度框 -->
<VDialog v-if="progressDialog" v-model="progressDialog" :scrim="false" width="25rem">
<VCard color="primary">
@@ -1477,35 +1598,5 @@ function onDragEndPlugin(evt: any) {
</template>
<style lang="scss" scoped>
// 拖拽相关样式
.drop-zone {
transition: all 0.3s ease;
&.drag-over {
transform: scale(1.02);
box-shadow: 0 0 20px rgba(33, 150, 243, 0.5);
border: 2px dashed #2196f3;
border-radius: 16px;
}
}
.plugin-item-wrapper {
position: relative;
.remove-from-folder-btn {
position: absolute;
top: 4px;
right: 4px;
z-index: 10;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(4px);
border-radius: 50%;
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover .remove-from-folder-btn {
opacity: 1;
}
}
// 样式已移至 PluginMixedSortCard 组件
</style>

View File

@@ -11,7 +11,6 @@ import router from '@/router'
import { useDisplay } from 'vuetify'
import { formatFileSize } from '@/@core/utils/formatters'
import { useI18n } from 'vue-i18n'
import { storageAttributes } from '@/api/constants'
// i18n
const { t } = useI18n()

View File

@@ -0,0 +1,470 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { TorrentCacheData, TorrentCacheItem } from '@/api/types'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
import { formatFileSize, formatDateDifference } from '@core/utils/formatters'
import { useConfirm } from '@/composables/useConfirm'
// 国际化
const { t } = useI18n()
const display = useDisplay()
const appMode = inject('pwaMode') && display.mdAndDown.value
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
// 确认框
const createConfirm = useConfirm()
// 提示框
const $toast = useToast()
// 缓存数据
const cacheData = ref<TorrentCacheData>({
count: 0,
sites: 0,
data: [],
})
// 筛选条件
const titleFilter = ref<string | null>(null)
const siteFilter = ref<string | null>(null)
// 获取所有站点选项
const siteOptions = computed(() => {
const sites = new Set<string>()
cacheData.value.data.forEach(item => {
if (item.site_name) {
sites.add(item.site_name)
}
})
return Array.from(sites).sort()
})
// 筛选后的数据
const filteredData = computed(() => {
return cacheData.value.data.filter(item => {
const titleMatch = !titleFilter.value || item.title?.toLowerCase().includes(titleFilter.value?.toLowerCase())
const siteMatch = !siteFilter.value || item.site_name === siteFilter.value
return titleMatch && siteMatch
})
})
// 选中的缓存项
const selectedItems = ref<string[]>([])
// 加载状态
const loading = ref(false)
// 重新识别对话框
const reidentifyDialog = ref(false)
const currentReidentifyItem = ref<TorrentCacheItem | null>(null)
const tmdbId = ref<number | undefined>()
const doubanId = ref<string | undefined>()
const tableStyle = computed(() => {
return appMode ? '' : 'height: calc(100vh - 21rem - env(safe-area-inset-bottom)'
})
// 调用API加载缓存数据
async function loadCacheData() {
try {
loading.value = true
const res: any = await api.get('torrent/cache')
cacheData.value = res.data
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.loadFailed'))
} finally {
loading.value = false
}
}
// 清空所有缓存
async function clearAllCache() {
const isConfirmed = await createConfirm({
type: 'warn',
title: t('common.confirm'),
content: t('setting.cache.clearConfirm'),
})
if (!isConfirmed) return
try {
loading.value = true
await api.delete('torrent/cache')
$toast.success(t('setting.cache.clearSuccess'))
await loadCacheData()
selectedItems.value = []
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.clearFailed'))
} finally {
loading.value = false
}
}
// 刷新缓存
async function refreshCache() {
try {
loading.value = true
const res: any = await api.post('torrent/cache/refresh')
$toast.success(res.message || t('setting.cache.refreshSuccess'))
await loadCacheData()
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.refreshFailed'))
} finally {
loading.value = false
}
}
// 删除选中的缓存项
async function deleteSelectedItems() {
if (selectedItems.value.length === 0) {
$toast.warning(t('setting.cache.selectDeleteWarning'))
return
}
try {
loading.value = true
const deletePromises = selectedItems.value.map(hash => {
const item = cacheData.value.data.find(d => d.hash === hash)
if (item) {
return api.delete(`torrent/cache/${item.domain}/${hash}`)
}
return Promise.resolve()
})
await Promise.all(deletePromises)
$toast.success(t('setting.cache.deleteSelectedSuccess', { count: selectedItems.value.length }))
await loadCacheData()
selectedItems.value = []
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.deleteSelectedFailed'))
} finally {
loading.value = false
}
}
// 删除单个缓存项
async function deleteSingleItem(item: TorrentCacheItem) {
try {
loading.value = true
await api.delete(`torrent/cache/${item.domain}/${item.hash}`)
$toast.success(t('setting.cache.deleteSuccess'))
await loadCacheData()
// 从选中列表中移除
const index = selectedItems.value.indexOf(item.hash)
if (index > -1) {
selectedItems.value.splice(index, 1)
}
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.deleteFailed'))
} finally {
loading.value = false
}
}
// 打开重新识别对话框
function openReidentifyDialog(item: TorrentCacheItem) {
currentReidentifyItem.value = item
tmdbId.value = undefined
doubanId.value = undefined
reidentifyDialog.value = true
}
// 重新识别
async function performReidentify() {
if (!currentReidentifyItem.value) return
try {
loading.value = true
const params: any = {}
if (tmdbId.value) params.tmdbid = tmdbId.value
if (doubanId.value) params.doubanid = doubanId.value
const res: any = await api.post(
`torrent/cache/reidentify/${currentReidentifyItem.value.domain}/${currentReidentifyItem.value.hash}`,
null,
{
params,
},
)
$toast.success(res.message || t('setting.cache.reidentifySuccess'))
await loadCacheData()
reidentifyDialog.value = false
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.reidentifyFailed'))
} finally {
loading.value = false
}
}
// 获取媒体类型颜色
function getMediaTypeColor(type: string): string {
switch (type) {
case t('setting.cache.mediaType.movie'):
return 'primary'
case t('setting.cache.mediaType.tv'):
return 'success'
default:
return 'default'
}
}
// 打开详情页面
function openPageUrl(url: string) {
window.open(url, '_blank')
}
onMounted(() => {
loadCacheData()
})
</script>
<template>
<VCard>
<VCardItem>
<VCardTitle>{{ t('setting.cache.title') }}</VCardTitle>
<VCardSubtitle>{{ t('setting.cache.subtitle') }}</VCardSubtitle>
<template #append>
<div class="d-flex gap-2">
<VBtn icon color="primary" :loading="loading" @click="refreshCache">
<VIcon>mdi-refresh</VIcon>
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.refresh') }}</VTooltip>
</VBtn>
<VBtn
icon
color="warning"
:loading="loading"
:disabled="selectedItems.length === 0"
@click="deleteSelectedItems"
>
<VIcon>mdi-delete-sweep</VIcon>
<VTooltip activator="parent" location="bottom"
>{{ t('setting.cache.deleteSelected') }} ({{ selectedItems.length }})</VTooltip
>
</VBtn>
<VBtn icon color="error" :loading="loading" @click="clearAllCache">
<VIcon>mdi-delete-variant</VIcon>
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.clearAll') }}</VTooltip>
</VBtn>
</div>
</template>
</VCardItem>
<!-- 筛选框 -->
<VCardText>
<VRow>
<VCol cols="6">
<VTextField
v-model="titleFilter"
:label="t('setting.cache.filterByTitle')"
prepend-inner-icon="mdi-magnify"
clearable
density="compact"
/>
</VCol>
<VCol cols="6">
<VSelect
v-model="siteFilter"
:label="t('setting.cache.filterBySite')"
:items="siteOptions"
prepend-inner-icon="mdi-web"
clearable
density="compact"
:placeholder="t('setting.cache.selectSite')"
/>
</VCol>
</VRow>
</VCardText>
<!-- 缓存列表 -->
<VDataTable
v-model="selectedItems"
:headers="[
{ title: '', key: 'data-table-select', sortable: false, width: '48px' },
{ title: t('setting.cache.poster'), key: 'poster', sortable: false, width: '80px' },
{ title: t('setting.cache.torrentTitle'), key: 'title', sortable: true },
{ title: t('setting.cache.site'), key: 'site_name', sortable: true, width: '120px' },
{ title: t('setting.cache.size'), key: 'size', sortable: true, width: '100px' },
{ title: t('setting.cache.publishTime'), key: 'pubdate', sortable: true, width: '150px' },
{ title: t('setting.cache.recognitionResult'), key: 'media_info', sortable: false, width: '200px' },
{ title: t('setting.cache.actions'), key: 'actions', sortable: false, width: '150px' },
]"
:items="filteredData"
:loading="loading"
item-value="hash"
show-select
hover
fixed-header
:items-per-page-text="t('common.itemsPerPage')"
:no-data-text="t('common.noDataText')"
:loading-text="t('common.loadingText')"
:style="tableStyle"
>
<!-- 全选复选框 -->
<template #header.data-table-select="{ allSelected, selectAll, someSelected }">
<VCheckbox
:indeterminate="someSelected && !allSelected"
:model-value="allSelected"
@update:model-value="(value: boolean | null) => selectAll(value as boolean)"
/>
</template>
<!-- 海报列 -->
<template #item.poster="{ item }">
<div class="text-center">
<VImg
v-if="item.poster_path"
:src="item.poster_path"
:alt="item.media_name || item.title"
cover
rounded="md"
class="w-12 my-1 ms-auto"
/>
<VIcon v-else size="x-large" color="grey-lighten-1">
{{ item.media_type === 'movie' ? 'mdi-movie-open' : 'mdi-television-play' }}
</VIcon>
</div>
</template>
<!-- 标题列 -->
<template #item.title="{ item }">
<div class="d-flex flex-column min-w-40">
<div class="text-subtitle-2 font-weight-bold">
{{ item.title }}
</div>
<div v-if="item.description" class="text-caption text-grey">
{{ item.description }}
</div>
<div v-if="item.season_episode || item.resource_term" class="text-caption text-primary mt-1">
{{ item.season_episode }} {{ item.resource_term }}
</div>
</div>
</template>
<!-- 大小列 -->
<template #item.size="{ item }">
{{ formatFileSize(item.size) }}
</template>
<!-- 发布时间列 -->
<template #item.pubdate="{ item }">
{{ formatDateDifference(item.pubdate || '') }}
</template>
<!-- 识别结果列 -->
<template #item.media_info="{ item }">
<div v-if="item.media_name" class="d-flex flex-column">
<div class="text-subtitle-2">
{{ item.media_name }}
<span v-if="item.media_year" class="text-caption text-grey"> ({{ item.media_year }}) </span>
</div>
<div>
<VChip v-if="item.media_type" :color="getMediaTypeColor(item.media_type)" size="x-small">
{{ item.media_type }}
</VChip>
</div>
</div>
<div v-else class="text-caption text-grey">
{{ t('setting.cache.unrecognized') }}
</div>
</template>
<!-- 操作列 -->
<template #item.actions="{ item }">
<div class="d-flex gap-1">
<VBtn icon size="small" color="primary" variant="text" @click="openReidentifyDialog(item)">
<VIcon size="16">mdi-text-recognition</VIcon>
</VBtn>
<VBtn icon size="small" color="error" variant="text" @click="deleteSingleItem(item)">
<VIcon size="16">mdi-delete</VIcon>
</VBtn>
<VBtn
v-if="item.page_url"
icon
size="small"
color="info"
variant="text"
@click="openPageUrl(item.page_url || '')"
target="_blank"
>
<VIcon size="16">mdi-open-in-new</VIcon>
</VBtn>
</div>
</template>
<!-- 空状态 -->
<template #no-data>
<div class="text-center pa-4">
<VIcon size="64" class="mb-4"> mdi-database-off </VIcon>
<div class="text-body-2 text-grey">
{{ t('setting.cache.noData') }}
</div>
</div>
</template>
</VDataTable>
</VCard>
<!-- 重新识别对话框 -->
<VDialog v-model="reidentifyDialog" scrollable max-width="35rem">
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon>mdi-text-recognition</VIcon>
</template>
<VCardTitle>{{ t('setting.cache.reidentifyDialog.title') }}</VCardTitle>
<VCardSubtitle>{{ currentReidentifyItem?.title }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="reidentifyDialog = false" />
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VTextField
v-if="globalSettings.RECOGNIZE_SOURCE === 'themoviedb'"
v-model="tmdbId"
:label="t('setting.cache.reidentifyDialog.tmdbId')"
:hint="t('setting.cache.reidentifyDialog.tmdbIdHint')"
clearable
prepend-inner-icon="mdi-id-card"
persistent-hint
/>
<VTextField
v-else
v-model="doubanId"
:label="t('setting.cache.reidentifyDialog.doubanId')"
:hint="t('setting.cache.reidentifyDialog.doubanIdHint')"
clearable
prepend-inner-icon="mdi-id-card"
persistent-hint
/>
</VCol>
</VRow>
<VAlert type="info" variant="tonal" class="mt-4">
{{ t('setting.cache.reidentifyDialog.autoHint') }}
</VAlert>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn color="primary" :loading="loading" prepend-icon="mdi-check" @click="performReidentify">
{{ t('setting.cache.reidentifyDialog.confirm') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -239,7 +239,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveStorages"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" class="me-2" @click="saveStorages" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal" @click="addStorage">
<VIcon icon="mdi-plus" />
</VBtn>
@@ -279,7 +281,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveDirectories"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveDirectories" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal" @click="addDirectory">
<VIcon icon="mdi-plus" />
</VBtn>
@@ -305,6 +309,7 @@ onMounted(() => {
:label="t('setting.directory.scrapSource')"
:hint="t('setting.directory.scrapSourceHint')"
persistent-hint
prepend-inner-icon="mdi-database"
/>
</VCol>
<VCol cols="12">
@@ -315,6 +320,7 @@ onMounted(() => {
persistent-hint
clearable
active
prepend-inner-icon="mdi-movie-open"
/>
</VCol>
<VCol cols="12">
@@ -325,6 +331,7 @@ onMounted(() => {
persistent-hint
clearable
active
prepend-inner-icon="mdi-television"
/>
</VCol>
</VRow>
@@ -332,7 +339,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSystemSettings(SystemSettings.Basic)"> {{ t('common.save') }}</VBtn>
<VBtn type="submit" @click="saveSystemSettings(SystemSettings.Basic)" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -293,7 +293,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn mtype="submit" @click="saveNotificationSetting"> {{ t('common.save') }} </VBtn>
<VBtn mtype="submit" @click="saveNotificationSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu :activator="'parent'" :close-on-content-click="true">
@@ -401,7 +403,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveNotificationSwitchs"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveNotificationSwitchs" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -418,17 +422,29 @@ onMounted(() => {
<VCardText>
<VRow>
<VCol cols="6">
<VTextField v-model="notificationTime.start" :label="t('setting.notification.startTime')" type="time" />
<VTextField
v-model="notificationTime.start"
:label="t('setting.notification.startTime')"
type="time"
prepend-inner-icon="mdi-clock-start"
/>
</VCol>
<VCol cols="6">
<VTextField v-model="notificationTime.end" :label="t('setting.notification.endTime')" type="time" />
<VTextField
v-model="notificationTime.end"
:label="t('setting.notification.endTime')"
type="time"
prepend-inner-icon="mdi-clock-end"
/>
</VCol>
</VRow>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveNotificationTime"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveNotificationTime" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -445,11 +461,16 @@ onMounted(() => {
<!-- 模板编辑器对话框 -->
<VDialog v-model="editorVisible" v-if="editorVisible" max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard>
<VCardItem>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-code-json" class="me-2" />
</template>
<VCardTitle>
{{ templateTypes.find(t => t.type === currentTemplate)?.label }}
{{ t('setting.notification.templateConfigTitle') }}
</VCardTitle>
<VCardSubtitle>
{{ templateTypes.find(t => t.type === currentTemplate)?.label }}
</VCardSubtitle>
<VDialogCloseBtn @click="editorVisible = false" />
</VCardItem>
<VCardText class="py-0">

View File

@@ -401,7 +401,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveCustomRules"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" class="me-2" @click="saveCustomRules" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtnGroup density="comfortable">
<VBtn color="success" variant="tonal" @click="addCustomRule">
<VIcon icon="mdi-plus" />
@@ -452,7 +454,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtnGroup density="comfortable">
<VBtn color="success" variant="tonal" @click="addFilterRuleGroup">
<VIcon icon="mdi-plus" />
@@ -501,6 +505,7 @@ onMounted(() => {
:label="t('setting.rule.currentPriorityRules')"
:hint="t('setting.rule.currentPriorityRulesHint')"
persistent-hint
prepend-inner-icon="mdi-priority-high"
/>
</VCol>
</VRow>
@@ -509,7 +514,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveTorrentPriority"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveTorrentPriority" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -205,6 +205,7 @@ onMounted(() => {
:label="t('setting.search.mediaSource')"
:hint="t('setting.search.mediaSourceHint')"
persistent-hint
prepend-inner-icon="mdi-database-search"
/>
</VCol>
<VCol cols="12" md="6">
@@ -217,6 +218,7 @@ onMounted(() => {
:label="t('setting.search.filterRuleGroup')"
:hint="t('setting.search.filterRuleGroupHint')"
persistent-hint
prepend-inner-icon="mdi-filter"
/>
</VCol>
</VRow>
@@ -228,6 +230,7 @@ onMounted(() => {
placeholder="MOVIEPILOT"
:hint="t('setting.search.downloadLabelHint')"
persistent-hint
prepend-inner-icon="mdi-tag"
/>
</VCol>
<VCol cols="12" md="6">
@@ -237,6 +240,7 @@ onMounted(() => {
:placeholder="t('setting.search.downloadUserPlaceholder')"
:hint="t('setting.search.downloadUserHint')"
persistent-hint
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -260,7 +264,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSearchSetting"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSearchSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -291,7 +297,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSelectedSites"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSelectedSites" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -161,6 +161,7 @@ onMounted(() => {
:disabled="siteSetting.CookieCloud.COOKIECLOUD_ENABLE_LOCAL"
:hint="t('setting.site.serviceAddressHint')"
persistent-hint
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -169,6 +170,7 @@ onMounted(() => {
:label="t('setting.site.userKey')"
:hint="t('setting.site.userKeyHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12" md="6">
@@ -180,6 +182,7 @@ onMounted(() => {
:label="t('setting.site.e2ePassword')"
:hint="t('setting.site.e2ePasswordHint')"
persistent-hint
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
@@ -189,6 +192,7 @@ onMounted(() => {
:items="CookieCloudIntervalItems"
:hint="t('setting.site.autoSyncIntervalHint')"
persistent-hint
prepend-inner-icon="mdi-timer"
/>
</VCol>
<VCol cols="12" md="6">
@@ -198,6 +202,7 @@ onMounted(() => {
:placeholder="t('setting.site.syncBlacklistPlaceholder')"
:hint="t('setting.site.syncBlacklistHint')"
persistent-hint
prepend-inner-icon="mdi-block-helper"
/>
</VCol>
<VCol cols="12" md="6">
@@ -206,6 +211,7 @@ onMounted(() => {
:label="t('setting.site.userAgent')"
:hint="t('setting.site.userAgentHint')"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
</VRow>
@@ -214,7 +220,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSiteSetting(siteSetting.CookieCloud)"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSiteSetting(siteSetting.CookieCloud)" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -234,6 +242,7 @@ onMounted(() => {
:items="SiteDataRefreshIntervalItems"
:hint="t('setting.site.siteDataRefreshIntervalHint')"
persistent-hint
prepend-inner-icon="mdi-refresh"
/>
</VCol>
</VRow>
@@ -252,7 +261,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSiteSetting(siteSetting.Site)"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSiteSetting(siteSetting.Site)" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -217,6 +217,7 @@ onMounted(() => {
:label="t('setting.subscribe.mode')"
:hint="t('setting.subscribe.modeHint')"
persistent-hint
prepend-inner-icon="mdi-cog"
/>
</VCol>
<VCol cols="12" md="6">
@@ -226,6 +227,7 @@ onMounted(() => {
:label="t('setting.subscribe.rssInterval')"
:hint="t('setting.subscribe.rssIntervalHint')"
persistent-hint
prepend-inner-icon="mdi-timer"
/>
</VCol>
<VCol cols="12" md="6">
@@ -238,6 +240,7 @@ onMounted(() => {
:label="t('setting.subscribe.filterRuleGroup')"
:hint="t('setting.subscribe.filterRuleGroupHint')"
persistent-hint
prepend-inner-icon="mdi-filter"
/>
</VCol>
<VCol cols="12" md="6">
@@ -250,6 +253,7 @@ onMounted(() => {
:label="t('setting.subscribe.bestVersionRuleGroup')"
:hint="t('setting.subscribe.bestVersionRuleGroupHint')"
persistent-hint
prepend-inner-icon="mdi-star"
/>
</VCol>
</VRow>
@@ -276,7 +280,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSubscribeSetting"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSubscribeSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -307,7 +313,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSelectedRssSites"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveSelectedRssSites" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -40,6 +40,8 @@ const SystemSettings = ref<any>({
PLUGIN_STATISTIC_SHARE: true,
BIG_MEMORY_MODE: false,
DB_WAL_ENABLE: false,
AUTO_UPDATE_RESOURCE: true,
MOVIEPILOT_AUTO_UPDATE: false,
// 媒体
TMDB_API_DOMAIN: null,
TMDB_IMAGE_DOMAIN: null,
@@ -385,6 +387,16 @@ function onMediaServerChange(mediaserver: MediaServerConf, name: string) {
if (index !== -1) mediaServers.value[index] = mediaserver
}
// 添加计算属性
const moviePilotAutoUpdate = computed({
get: () => {
return ['release', 'dev'].includes(SystemSettings.value.Advanced.MOVIEPILOT_AUTO_UPDATE)
},
set: val => {
SystemSettings.value.Advanced.MOVIEPILOT_AUTO_UPDATE = val ? 'release' : 'false'
},
})
// 加载数据
onMounted(() => {
loadDownloaderSetting()
@@ -426,6 +438,7 @@ onDeactivated(() => {
:hint="t('setting.system.appDomainHint')"
placeholder="http://localhost:3000"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
@@ -438,6 +451,7 @@ onDeactivated(() => {
:hint="t('setting.system.wallpaperHint')"
persistent-hint
:items="wallpaperItems"
prepend-inner-icon="mdi-image"
/>
</VCol>
@@ -449,6 +463,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.customizeWallpaperApi')"
persistent-hint
:rules="[v => !!v || t('setting.system.customizeWallpaperApiRequired')]"
prepend-inner-icon="mdi-api"
/>
</VCol>
</VRow>
@@ -463,6 +478,7 @@ onDeactivated(() => {
{ title: 'TheMovieDb', value: 'themoviedb' },
{ title: '豆瓣', value: 'douban' },
]"
prepend-inner-icon="mdi-database"
/>
</VCol>
<VCol cols="12" md="6">
@@ -479,6 +495,7 @@ onDeactivated(() => {
(v: any) => !isNaN(v) || t('setting.system.numbersOnly'),
(v: any) => v >= 1 || t('setting.system.minInterval'),
]"
prepend-inner-icon="mdi-sync"
/>
</VCol>
<VCol cols="12" md="6">
@@ -488,10 +505,11 @@ onDeactivated(() => {
:hint="t('setting.system.apiTokenHint')"
:placeholder="t('setting.system.apiTokenMinChars')"
persistent-hint
prependInnerIcon="mdi-reload"
:appendInnerIcon="SystemSettings.Basic.API_TOKEN ? 'mdi-content-copy' : ''"
@click:prependInner="createRandomString"
@click:appendInner="copyValue(SystemSettings.Basic.API_TOKEN)"
prepend-inner-icon="mdi-key"
:append-inner-icon="SystemSettings.Basic.API_TOKEN ? 'mdi-content-copy' : 'mdi-reload'"
@click:append-inner="
SystemSettings.Basic.API_TOKEN ? copyValue(SystemSettings.Basic.API_TOKEN) : createRandomString()
"
:rules="[
(v: string) => !!v || t('setting.system.apiTokenRequired'),
(v: string) => v.length >= 16 || t('setting.system.apiTokenLength'),
@@ -505,6 +523,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.githubTokenFormat')"
:hint="t('setting.system.githubTokenHint')"
persistent-hint
prepend-inner-icon="mdi-github"
>
</VTextField>
</VCol>
@@ -515,6 +534,7 @@ onDeactivated(() => {
placeholder="https://movie-pilot.org"
:hint="t('setting.system.ocrHostHint')"
persistent-hint
prepend-inner-icon="mdi-text-recognition"
/>
</VCol>
</VRow>
@@ -523,7 +543,9 @@ onDeactivated(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveBasicSettings"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveBasicSettings" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VSpacer />
<VBtn
color="error"
@@ -568,7 +590,9 @@ onDeactivated(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveDownloaderSetting"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveDownloaderSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
@@ -616,7 +640,9 @@ onDeactivated(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveMediaServerSetting"> {{ t('common.save') }} </VBtn>
<VBtn type="submit" @click="saveMediaServerSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
@@ -721,6 +747,22 @@ onDeactivated(() => {
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="moviePilotAutoUpdate"
:label="t('setting.system.moviePilotAutoUpdate')"
:hint="t('setting.system.moviePilotAutoUpdateHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.AUTO_UPDATE_RESOURCE"
:label="t('setting.system.autoUpdateResource')"
:hint="t('setting.system.autoUpdateResourceHint')"
persistent-hint
/>
</VCol>
</VRow>
</div>
</VWindowItem>
@@ -736,6 +778,7 @@ onDeactivated(() => {
persistent-hint
:items="['api.themoviedb.org', 'api.tmdb.org']"
:rules="[(v: string) => !!v || t('setting.system.tmdbApiDomainRequired')]"
prepend-inner-icon="mdi-api"
/>
</VCol>
<VCol cols="12" md="6">
@@ -747,6 +790,7 @@ onDeactivated(() => {
persistent-hint
:items="['image.tmdb.org', 'static-mdb.v.geilijiasu.com']"
:rules="[(v: string) => !!v || t('setting.system.tmdbImageDomainRequired')]"
prepend-inner-icon="mdi-image"
/>
</VCol>
<VCol cols="12" md="6">
@@ -757,6 +801,7 @@ onDeactivated(() => {
:hint="t('setting.system.tmdbLocaleHint')"
persistent-hint
:items="tmdbLanguageItems"
prepend-inner-icon="mdi-translate"
/>
</VCol>
<VCol cols="12" md="6">
@@ -772,6 +817,7 @@ onDeactivated(() => {
(v: any) => v === 0 || !!v || t('setting.system.metaCacheExpireRequired'),
(v: any) => v >= 0 || t('setting.system.metaCacheExpireMin'),
]"
prepend-inner-icon="mdi-timer"
/>
</VCol>
</VRow>
@@ -806,6 +852,16 @@ onDeactivated(() => {
<VWindowItem value="network">
<div>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Advanced.PROXY_HOST"
:label="t('setting.system.proxyHost')"
placeholder="http://127.0.0.1:7890"
:hint="t('setting.system.proxyHostHint')"
persistent-hint
prepend-inner-icon="mdi-server-network"
/>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="githubProxyDisplay"
@@ -815,9 +871,10 @@ onDeactivated(() => {
persistent-hint
:items="githubMirrorsItems"
clearable
prepend-inner-icon="mdi-github"
/>
</VCol>
<VCol cols="12" md="6">
<VCol cols="12">
<VCombobox
v-model="pipProxyDisplay"
:label="t('setting.system.pipProxy')"
@@ -826,6 +883,7 @@ onDeactivated(() => {
persistent-hint
:items="pipMirrorsItems"
clearable
prepend-inner-icon="mdi-package"
/>
</VCol>
</VRow>
@@ -845,6 +903,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.dohResolversPlaceholder')"
:hint="t('setting.system.dohResolversHint')"
persistent-hint
prepend-inner-icon="mdi-dns"
/>
</VCol>
<VCol cols="12" v-show="SystemSettings.Advanced.DOH_ENABLE">
@@ -854,6 +913,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.dohDomainsPlaceholder')"
:hint="t('setting.system.dohDomainsHint')"
persistent-hint
prepend-inner-icon="mdi-domain"
/>
</VCol>
</VRow>
@@ -885,6 +945,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.securityImageDomainAdd')"
hide-details
density="compact"
prepend-inner-icon="mdi-shield-check"
>
<template #append>
<VBtn icon color="primary" @click="addSecurityDomain" :disabled="!newSecurityDomain">
@@ -918,6 +979,7 @@ onDeactivated(() => {
:hint="t('setting.system.logLevelHint')"
persistent-hint
:items="logLevelItems"
prepend-inner-icon="mdi-format-list-bulleted"
/>
</VCol>
<VCol cols="12" md="6">
@@ -930,6 +992,7 @@ onDeactivated(() => {
type="number"
:suffix="t('setting.system.mb')"
:rules="[(v: any) => v === 0 || !!v || t('setting.system.logMaxFileSizeRequired'), (v: any) => v >= 1 || t('setting.system.logMaxFileSizeMin')]"
prepend-inner-icon="mdi-file-document"
/>
</VCol>
<VCol cols="12" md="6">
@@ -941,6 +1004,7 @@ onDeactivated(() => {
min="1"
type="number"
:rules="[(v: any) => v === 0 || !!v || t('setting.system.logBackupCountRequired'), (v: any) => v >= 1 || t('setting.system.logBackupCountMin')]"
prepend-inner-icon="mdi-backup-restore"
/>
</VCol>
<VCol cols="12">
@@ -949,6 +1013,7 @@ onDeactivated(() => {
:label="t('setting.system.logFileFormat')"
:hint="t('setting.system.logFileFormatHint')"
persistent-hint
prepend-inner-icon="mdi-format-text"
/>
</VCol>
</VRow>

View File

@@ -143,6 +143,7 @@ onMounted(() => {
:placeholder="t('setting.words.identifiersPlaceholder')"
:hint="t('setting.words.identifiersHint')"
persistent-hint
prepend-inner-icon="mdi-tag-text"
/>
</VCardText>
<VCardText>
@@ -153,7 +154,9 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomIdentifiers">{{ t('common.save') }}</VBtn>
<VBtn type="submit" @click="saveCustomIdentifiers" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -173,12 +176,15 @@ onMounted(() => {
:placeholder="t('setting.words.releaseGroupsPlaceholder')"
:hint="t('setting.words.releaseGroupsHint')"
persistent-hint
prepend-inner-icon="mdi-account-group"
/>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomReleaseGroups">{{ t('common.save') }}</VBtn>
<VBtn type="submit" @click="saveCustomReleaseGroups" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -198,12 +204,15 @@ onMounted(() => {
:placeholder="t('setting.words.customizationPlaceholder')"
:hint="t('setting.words.customizationHint')"
persistent-hint
prepend-inner-icon="mdi-code-braces"
/>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomization">{{ t('common.save') }}</VBtn>
<VBtn type="submit" @click="saveCustomization" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
@@ -223,12 +232,15 @@ onMounted(() => {
:placeholder="t('setting.words.excludeWordsPlaceholder')"
:hint="t('setting.words.excludeWordsHint')"
persistent-hint
prepend-inner-icon="mdi-block-helper"
/>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveTransferExcludeWords">{{ t('common.save') }}</VBtn>
<VBtn type="submit" @click="saveTransferExcludeWords" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -1,6 +1,11 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
// 定义输入变量
const props = defineProps<{
logfile: string
}>()
// 国际化
const { t } = useI18n()
@@ -33,7 +38,12 @@ function getLogColor(level: string): string {
// SSE持续获取日志
function startSSELogging() {
eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/logging`)
console.log(props.logfile)
eventSource = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/logging?logfile=${
encodeURIComponent(props.logfile) ?? 'moviepilot.log'
}`,
)
const buffer: string[] = []
let timeoutId: number | null = null

View File

@@ -54,10 +54,21 @@ async function nameTest() {
<VForm @submit.prevent="() => {}">
<VRow class="pt-2">
<VCol cols="12">
<VTextField v-model="nameTestForm.title" :label="t('nameTest.title')" :rules="[requiredValidator]" />
<VTextField
v-model="nameTestForm.title"
:label="t('nameTest.title')"
:rules="[requiredValidator]"
prepend-inner-icon="mdi-movie-open"
/>
</VCol>
<VCol cols="12">
<VTextarea v-model="nameTestForm.subtitle" :label="t('nameTest.subtitle')" rows="2" auto-grow />
<VTextarea
v-model="nameTestForm.subtitle"
:label="t('nameTest.subtitle')"
rows="2"
auto-grow
prepend-inner-icon="mdi-subtitles"
/>
</VCol>
</VRow>
<VRow>

View File

@@ -80,13 +80,29 @@ onMounted(() => {
<VForm @submit.prevent="() => {}">
<VRow class="pt-2">
<VCol cols="12" md="8">
<VTextField v-model="ruleTestForm.title" :label="t('ruleTest.title')" :rules="[requiredValidator]" />
<VTextField
v-model="ruleTestForm.title"
:label="t('ruleTest.title')"
:rules="[requiredValidator]"
prepend-inner-icon="mdi-movie-open"
/>
</VCol>
<VCol cols="12" md="4">
<VSelect v-model="ruleTestForm.rulegroup" :label="t('ruleTest.ruleGroup')" :items="filterRuleGroupItems" />
<VSelect
v-model="ruleTestForm.rulegroup"
:label="t('ruleTest.ruleGroup')"
:items="filterRuleGroupItems"
prepend-inner-icon="mdi-filter"
/>
</VCol>
<VCol cols="12">
<VTextarea v-model="ruleTestForm.subtitle" :label="t('ruleTest.subtitle')" rows="2" auto-grow />
<VTextarea
v-model="ruleTestForm.subtitle"
:label="t('ruleTest.subtitle')"
rows="2"
auto-grow
prepend-inner-icon="mdi-subtitles"
/>
</VCol>
</VRow>
<VRow>

View File

@@ -322,7 +322,13 @@ watch(
<VForm class="mt-6">
<VRow>
<VCol cols="12" md="6">
<VTextField v-model="currentUserName" density="comfortable" readonly :label="t('user.username')" />
<VTextField
v-model="currentUserName"
density="comfortable"
readonly
:label="t('user.username')"
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
@@ -331,6 +337,7 @@ watch(
clearable
:label="t('user.email')"
type="email"
prepend-inner-icon="mdi-email"
/>
</VCol>
<VCol cols="12" md="6">
@@ -342,6 +349,7 @@ watch(
clearable
:label="t('user.password')"
autocomplete=""
prepend-inner-icon="mdi-lock"
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
@@ -354,6 +362,7 @@ watch(
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
clearable
:label="t('user.confirmPassword')"
prepend-inner-icon="mdi-lock-check"
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
/>
</VCol>
@@ -364,6 +373,7 @@ watch(
clearable
:label="t('profile.nickname')"
:placeholder="t('profile.nicknamePlaceholder')"
prepend-inner-icon="mdi-card-account-details"
/>
</VCol>
</VRow>
@@ -379,6 +389,7 @@ watch(
density="comfortable"
clearable
:label="t('profile.wechatUser')"
prepend-inner-icon="mdi-wechat"
/>
</VCol>
<VCol cols="12" md="6">
@@ -387,6 +398,7 @@ watch(
density="comfortable"
clearable
:label="t('profile.telegramUser')"
prepend-inner-icon="mdi-send"
/>
</VCol>
<VCol cols="12" md="6">
@@ -395,6 +407,7 @@ watch(
density="comfortable"
clearable
:label="t('profile.slackUser')"
prepend-inner-icon="mdi-slack"
/>
</VCol>
<VCol cols="12" md="6">
@@ -403,6 +416,7 @@ watch(
density="comfortable"
clearable
:label="t('profile.vocechatUser')"
prepend-inner-icon="mdi-chat"
/>
</VCol>
<VCol cols="12" md="6">
@@ -411,6 +425,7 @@ watch(
density="comfortable"
clearable
:label="t('profile.synologychatUser')"
prepend-inner-icon="mdi-message"
/>
</VCol>
<VCol cols="12" md="6">
@@ -419,13 +434,14 @@ watch(
density="comfortable"
clearable
:label="t('profile.doubanUser')"
prepend-inner-icon="mdi-movie"
/>
</VCol>
</VRow>
<VRow>
<!-- 👉 Form Actions -->
<VCol cols="12" class="d-flex flex-wrap gap-4">
<VBtn @click="saveAccountInfo" :disabled="isSaving">
<VBtn @click="saveAccountInfo" :disabled="isSaving" prepend-icon="mdi-content-save">
<span v-if="isSaving">{{ t('common.saving') }}...</span>
<span v-else>{{ t('common.save') }}</span>
</VBtn>
@@ -462,6 +478,7 @@ watch(
autocomplete=""
class="mb-8"
variant="outlined"
prepend-inner-icon="mdi-shield-key"
/>
<div class="d-flex justify-end flex-wrap gap-4">
<VBtn variant="outlined" color="secondary" @click="otpDialog = false"> {{ t('common.cancel') }} </VBtn>