Compare commits

..

61 Commits

Author SHA1 Message Date
jxxghp
2bc52576d9 更新package.json中的版本号 2025-05-29 08:23:18 +08:00
jxxghp
700d2c4a51 刷新数据时重新加载文件夹配置,以确保插件正确显示。 2025-05-29 08:21:17 +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
jxxghp
6fbd41f40a 优化 PluginAppCard 和 PluginCard 组件的样式 2025-05-25 20:57:42 +08:00
jxxghp
0181f614e1 为 SiteCard 和 SubscribeCard 组件添加显示器宽度逻辑,优化图标的鼠标移动样式 2025-05-25 19:50:57 +08:00
jxxghp
fded7b0b28 为多个组件的对话框添加全屏显示逻辑 2025-05-25 19:44:04 +08:00
jxxghp
7e637f835a 优化 TorrentCardListView 和 TorrentRowListView 组件的确认按钮样式 2025-05-25 15:51:24 +08:00
jxxghp
deaaf1834d 为 v-table 组件的表头添加背景模糊效果和背景色,提升视觉效果 2025-05-25 15:01:28 +08:00
jxxghp
139c870f99 更新 MediaServerCard.vue 2025-05-25 11:01:26 +08:00
jxxghp
4cc2350bc6 移除 SiteResourceDialog 组件中的分页文本绑定 2025-05-25 09:17:00 +08:00
jxxghp
8b31a118da 为英文和中文语言文件添加分页文本格式,提升用户界面信息展示 2025-05-24 22:28:58 +08:00
jxxghp
cca26acb78 更新 PluginFolderCard 和 PluginCardListView 组件的默认渐变背景颜色,提升视觉效果 2025-05-24 20:09:43 +08:00
jxxghp
245edbd2f6 优化 PluginAppCard 组件的文本显示方式 2025-05-24 20:06:11 +08:00
jxxghp
903d22c622 优化多个组件的样式和结构,调整文本显示方式,提升用户界面体验 2025-05-24 20:01:20 +08:00
jxxghp
8b1805628e 为 PluginFolderCard 组件添加背景图片计算逻辑和背景遮罩样式,优化背景显示效果 2025-05-24 17:36:32 +08:00
jxxghp
11c8c488da 调整 ConfirmDialog 组件的宽度属性 2025-05-24 17:22:49 +08:00
jxxghp
4dd4e0e148 自实现 UseConfirm 组件 2025-05-24 17:19:43 +08:00
jxxghp
21f352aa64 优化 PluginAppCard 组件,添加插件标签显示功能;调整 PluginFolderCard 组件的菜单位置和图标样式;更新 PluginCardListView 组件的文件夹显示逻辑。 2025-05-24 16:38:34 +08:00
jxxghp
6c4beffdb7 优化多个组件的按钮样式 2025-05-24 15:37:40 +08:00
jxxghp
43d3efa838 优化 PluginFolderCard 组件 2025-05-24 14:47:47 +08:00
jxxghp
1c99839ab4 更新版本号至 2.5.0 2025-05-24 14:20:36 +08:00
jxxghp
c9e05ce5b1 调整 PluginFolderCard 组件的最小高度属性,从 9rem 修改为 8.5rem 2025-05-24 14:11:39 +08:00
jxxghp
3fe7ed0e1d 优化多个组件中的按钮样式 2025-05-24 14:06:10 +08:00
jxxghp
b3bff5c6f5 移除 PluginCardListView 组件中的调试日志,优化错误处理逻辑 2025-05-24 14:06:10 +08:00
jxxghp
e357bac70f 为文件夹功能添加国际化支持 2025-05-24 14:06:10 +08:00
jxxghp
ad51d4e4f3 调整 PluginCardListView 组件的样式 2025-05-24 14:06:10 +08:00
jxxghp
912d8ced93 更新 PluginFolderCard 组件,添加国际化支持 2025-05-24 14:06:10 +08:00
jxxghp
8334999e98 优化 PluginAppCard、PluginCard 和 PluginFolderCard 组件的样式,调整布局和响应式设计 2025-05-24 14:06:10 +08:00
jxxghp
5e23ea7809 更新 NotificationChannelCard.vue 2025-05-24 09:43:47 +08:00
jxxghp
b62d291aab Merge pull request #338 from madrays/v2 2025-05-24 06:34:30 +08:00
madrays
a34dd8148f 重构插件页面,增加文件夹功能 2025-05-24 03:58:14 +08:00
75 changed files with 3423 additions and 562 deletions

View File

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

1
components.d.ts vendored
View File

@@ -8,6 +8,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ConfirmDialog: typeof import('./src/@core/components/ConfirmDialog.vue')['default']
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default']
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.4.9",
"version": "2.5.1-1",
"private": true,
"type": "module",
"bin": "dist/service.js",
@@ -62,7 +62,6 @@
"vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0",
"vuetify": "3.7.3",
"vuetify-use-dialog": "^0.6.11",
"webfontloader": "^1.6.28"
},
"devDependencies": {

View File

@@ -0,0 +1,86 @@
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
modelValue: boolean
type?: 'info' | 'warn' | 'error'
title?: string
content?: string
confirmText?: string
cancelText?: string
width?: string | number
}
const props = withDefaults(defineProps<Props>(), {
type: 'info',
title: '',
content: '',
confirmText: '',
cancelText: '',
width: '28rem',
})
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'confirm'): void
(e: 'cancel'): void
}>()
// 对话框类型对应的图标和颜色
const typeConfig = {
info: {
icon: 'mdi-information',
color: 'info',
},
warn: {
icon: 'mdi-alert',
color: 'warning',
},
error: {
icon: 'mdi-alert-circle',
color: 'error',
},
}
// 获取当前类型的配置
const currentType = computed(() => typeConfig[props.type])
// 确认按钮点击
function handleConfirm() {
emit('confirm')
emit('update:modelValue', false)
}
// 取消按钮点击
function handleCancel() {
emit('cancel')
emit('update:modelValue', false)
}
</script>
<template>
<VDialog :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" :max-width="width">
<VCard>
<VCardItem>
<div class="d-flex align-center justify-start mt-3">
<VAvatar :color="currentType.color" variant="text" size="x-large">
<VIcon size="x-large" :icon="currentType.icon" />
</VAvatar>
<div class="mx-3">
<p class="font-weight-bold text-xl text-high-emphasis">{{ title }}</p>
<p>{{ content }}</p>
</div>
</div>
</VCardItem>
<VCardActions class="mx-auto">
<VBtn variant="tonal" color="secondary" class="px-5" @click="handleCancel">
{{ cancelText }}
</VBtn>
<VBtn variant="elevated" :color="currentType.color" @click="handleConfirm" class="px-5">
{{ confirmText }}
</VBtn>
</VCardActions>
<VDialogCloseBtn @click="handleCancel" />
</VCard>
</VDialog>
</template>

View File

@@ -5,6 +5,10 @@ import filter_svg from '@images/svg/filter.svg'
import { cloneDeep } from 'lodash-es'
import { innerFilterRules } from '@/api/constants'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
@@ -106,8 +110,20 @@ function onClose() {
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-if="ruleInfoDialog" v-model="ruleInfoDialog" scrollable max-width="40rem">
<VCard :title="t('customRule.title', { id: props.rule.id })">
<VDialog
v-if="ruleInfoDialog"
v-model="ruleInfoDialog"
scrollable
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<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>
@@ -121,6 +137,7 @@ function onClose() {
:hint="t('customRule.hint.ruleId')"
persistent-hint
active
prepend-inner-icon="mdi-identifier"
/>
</VCol>
<VCol cols="12" md="6">
@@ -131,6 +148,7 @@ function onClose() {
:hint="t('customRule.hint.ruleName')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12">
@@ -141,6 +159,7 @@ function onClose() {
:hint="t('customRule.hint.include')"
persistent-hint
active
prepend-inner-icon="mdi-plus-circle"
/>
</VCol>
<VCol cols="12">
@@ -151,6 +170,7 @@ function onClose() {
:hint="t('customRule.hint.exclude')"
persistent-hint
active
prepend-inner-icon="mdi-minus-circle"
/>
</VCol>
<VCol cols="6">
@@ -161,6 +181,7 @@ function onClose() {
:hint="t('customRule.hint.sizeRange')"
persistent-hint
active
prepend-inner-icon="mdi-harddisk"
/>
</VCol>
<VCol cols="6">
@@ -171,6 +192,7 @@ function onClose() {
:hint="t('customRule.hint.seeders')"
persistent-hint
active
prepend-inner-icon="mdi-account-group"
/>
</VCol>
<VCol cols="6">
@@ -181,13 +203,14 @@ function onClose() {
:hint="t('customRule.hint.publishTime')"
persistent-hint
active
prepend-inner-icon="mdi-calendar-clock"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VBtn @click="saveRuleInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">{{
<VBtn @click="saveRuleInfo" prepend-icon="mdi-content-save" class="px-5">{{
t('customRule.action.confirm')
}}</VBtn>
</VCardActions>

View File

@@ -10,6 +10,10 @@ import custom_image from '@images/logos/downloader.png'
import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { downloaderDict } from '@/api/constants'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 获取i18n实例
const { t } = useI18n()
@@ -188,8 +192,22 @@ onUnmounted(() => {
</VCardText>
</VCard>
</VHover>
<VDialog v-if="downloaderInfoDialog" v-model="downloaderInfoDialog" scrollable max-width="40rem">
<VCard :title="`${props.downloader.name} - ${t('downloader.title')}`">
<VDialog
v-if="downloaderInfoDialog"
v-model="downloaderInfoDialog"
scrollable
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<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>
@@ -215,6 +233,7 @@ onUnmounted(() => {
:hint="t('downloader.name')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -225,6 +244,7 @@ onUnmounted(() => {
:hint="t('downloader.host')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -234,6 +254,7 @@ onUnmounted(() => {
:hint="t('downloader.username')"
persistent-hint
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -244,6 +265,7 @@ onUnmounted(() => {
:hint="t('downloader.password')"
persistent-hint
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12" md="6">
@@ -292,6 +314,7 @@ onUnmounted(() => {
:hint="t('downloader.name')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -302,6 +325,7 @@ onUnmounted(() => {
:hint="t('downloader.host')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -311,6 +335,7 @@ onUnmounted(() => {
:hint="t('downloader.username')"
persistent-hint
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
@@ -321,6 +346,7 @@ onUnmounted(() => {
:hint="t('downloader.password')"
persistent-hint
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
</VRow>
@@ -332,6 +358,7 @@ onUnmounted(() => {
:hint="t('downloader.customTypeHint')"
persistent-hint
active
prepend-inner-icon="mdi-cog"
/>
</VCol>
<VCol cols="12" md="6">
@@ -341,13 +368,14 @@ onUnmounted(() => {
:hint="t('downloader.nameRequired')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VBtn @click="saveDownloaderInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
<VBtn @click="saveDownloaderInfo" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>

View File

@@ -8,6 +8,10 @@ import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
import filter_group_svg from '@images/svg/filter-group.svg'
import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 获取i18n实例
const { t } = useI18n()
@@ -219,7 +223,13 @@ function onClose() {
<VImg :src="filter_group_svg" cover class="mt-10" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-if="groupInfoDialog" v-model="groupInfoDialog" scrollable max-width="80rem">
<VDialog
v-if="groupInfoDialog"
v-model="groupInfoDialog"
scrollable
max-width="80rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard :title="`${props.group.name} - ${t('filterRule.title')}`">
<VDialogCloseBtn v-model="groupInfoDialog" />
<VDivider />
@@ -233,6 +243,7 @@ function onClose() {
:hint="t('filterRule.groupName')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="6" md="3">
@@ -243,6 +254,7 @@ function onClose() {
:hint="t('filterRule.mediaType')"
persistent-hint
active
prepend-inner-icon="mdi-movie-open"
/>
</VCol>
<VCol cols="6" md="3">
@@ -253,6 +265,7 @@ function onClose() {
:hint="t('filterRule.category')"
persistent-hint
active
prepend-inner-icon="mdi-folder-open"
/>
</VCol>
</VRow>
@@ -280,17 +293,17 @@ function onClose() {
<div class="text-center" v-if="filterRuleCards.length == 0">{{ t('filterRule.add') }}</div>
</VCardText>
<VCardActions class="pt-3">
<VBtn color="primary" variant="tonal" @click="addFilterCard">
<VBtn color="primary" @click="addFilterCard">
<VIcon icon="mdi-plus" />
</VBtn>
<VBtn color="success" variant="tonal" @click="importRules('priority')">
<VBtn color="success" @click="importRules('priority')">
<VIcon icon="mdi-import" />
</VBtn>
<VBtn color="info" variant="tonal" @click="shareRules">
<VBtn color="info" @click="shareRules">
<VIcon icon="mdi-share" />
</VBtn>
<VSpacer />
<VBtn @click="saveGroupInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
<VBtn @click="saveGroupInfo" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>

View File

@@ -10,6 +10,10 @@ import api from '@/api'
import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { mediaServerDict } from '@/api/constants'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 获取i18n实例
const { t } = useI18n()
@@ -199,8 +203,22 @@ onMounted(() => {
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
</VCardText>
</VCard>
<VDialog v-if="mediaServerInfoDialog" v-model="mediaServerInfoDialog" scrollable max-width="40rem">
<VCard :title="`${props.mediaserver.name} - ${t('common.config')}`">
<VDialog
v-if="mediaServerInfoDialog"
v-model="mediaServerInfoDialog"
scrollable
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<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>
@@ -219,6 +237,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -229,6 +248,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -239,6 +259,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -248,6 +269,7 @@ onMounted(() => {
:hint="t('mediaserver.embyApiKeyHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
@@ -262,6 +284,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -275,6 +298,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -285,6 +309,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -295,6 +320,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -304,6 +330,7 @@ onMounted(() => {
:hint="t('mediaserver.jellyfinApiKeyHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
@@ -318,6 +345,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -331,6 +359,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -341,6 +370,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12">
@@ -351,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
@@ -362,6 +398,7 @@ onMounted(() => {
v-model="mediaServerInfo.config.password"
:label="t('mediaserver.password')"
active
prepend-inner-icon="mdi-lock"
/>
</VCol>
<VCol cols="12">
@@ -376,6 +413,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -389,6 +427,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12" md="6">
@@ -399,6 +438,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')"
persistent-hint
active
prepend-inner-icon="mdi-server"
/>
</VCol>
<VCol cols="12" md="6">
@@ -409,6 +449,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')"
persistent-hint
active
prepend-inner-icon="mdi-play-network"
/>
</VCol>
<VCol cols="12" md="6">
@@ -418,6 +459,7 @@ onMounted(() => {
:hint="t('mediaserver.plexTokenHint')"
persistent-hint
active
prepend-inner-icon="mdi-key"
/>
</VCol>
<VCol cols="12">
@@ -432,21 +474,7 @@ onMounted(() => {
persistent-hint
active
append-inner-icon="mdi-refresh"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
<VCol cols="12">
<VSelect
v-model="mediaServerInfo.sync_libraries"
:label="t('mediaserver.syncLibraries')"
:items="librariesOptions"
chips
multiple
clearable
:hint="t('mediaserver.syncLibrariesHint')"
persistent-hint
active
append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)"
/>
</VCol>
@@ -458,16 +486,22 @@ 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>
</VCardText>
<VCardActions class="pt-3">
<VBtn @click="saveMediaServerInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
<VBtn @click="saveMediaServerInfo" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.confirm') }}
</VBtn>
</VCardActions>

View File

@@ -10,6 +10,10 @@ import custom_image from '@images/logos/notification.png'
import { useToast } from 'vue-toast-notification'
import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
const { t } = useI18n()
@@ -106,8 +110,6 @@ const getIcon = computed(() => {
return slack_image
case 'webpush':
return chrome_image
case 'wechat':
return wechat_image
default:
return custom_image
}
@@ -138,9 +140,23 @@ function onClose() {
<VImg :src="getIcon" cover class="mt-7 me-1" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-if="notificationInfoDialog" v-model="notificationInfoDialog" scrollable max-width="40rem">
<VCard :title="`${props.notification.name} - ${t('notification.config')}`">
<VDialogCloseBtn v-model="notificationInfoDialog" />
<VDialog
v-if="notificationInfoDialog"
v-model="notificationInfoDialog"
scrollable
max-width="40rem"
:fullscreen="!display.mdAndUp.value"
>
<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>
@@ -158,6 +174,7 @@ function onClose() {
clearable
chips
persistent-hint
prepend-inner-icon="mdi-bell-outline"
/>
</VCol>
</VRow>
@@ -169,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">
@@ -177,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">
@@ -185,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">
@@ -193,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">
@@ -201,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">
@@ -209,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">
@@ -217,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">
@@ -226,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>
@@ -237,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">
@@ -245,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">
@@ -253,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">
@@ -262,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">
@@ -271,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>
@@ -282,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">
@@ -291,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">
@@ -300,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">
@@ -309,6 +342,7 @@ function onClose() {
:placeholder="t('notification.slack.channelPlaceholder')"
:hint="t('notification.slack.channelHint')"
persistent-hint
prepend-inner-icon="mdi-pound"
/>
</VCol>
</VRow>
@@ -320,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">
@@ -328,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">
@@ -336,6 +372,7 @@ function onClose() {
:label="t('notification.synologychat.token')"
:hint="t('notification.synologychat.tokenHint')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
</VRow>
@@ -347,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">
@@ -355,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">
@@ -363,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">
@@ -372,6 +412,7 @@ function onClose() {
:placeholder="t('notification.vocechat.channelIdPlaceholder')"
:hint="t('notification.vocechat.channelIdHint')"
persistent-hint
prepend-inner-icon="mdi-pound"
/>
</VCol>
</VRow>
@@ -383,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">
@@ -391,6 +433,7 @@ function onClose() {
:label="t('notification.webpush.username')"
:hint="t('notification.webpush.usernameHint')"
persistent-hint
prepend-inner-icon="mdi-account"
/>
</VCol>
</VRow>
@@ -402,6 +445,7 @@ function onClose() {
:hint="t('notification.customTypeHint')"
persistent-hint
active
prepend-inner-icon="mdi-cog"
/>
</VCol>
<VCol cols="12" md="6">
@@ -410,14 +454,14 @@ function onClose() {
:label="t('notification.name')"
:hint="t('notification.nameRequired')"
persistent-hint
active
prepend-inner-icon="mdi-label"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VBtn @click="saveNotificationInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
<VBtn @click="saveNotificationInfo" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.confirm') }}
</VBtn>
</VCardActions>

View File

@@ -98,10 +98,7 @@ function goPersonDetail() {
<div class="w-full truncate text-center font-bold">
{{ getPersonName() }}
</div>
<div
class="overflow-hidden whitespace-normal text-center text-sm"
style="display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2"
>
<div class="overflow-hidden whitespace-normal text-center text-sm text-ellipsis line-clamp-2">
{{ getPersonCharacter() }}
</div>
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />

View File

@@ -36,7 +36,17 @@ const $toast = useToast()
const progressDialog = ref(false)
// 进度框文本
const progressText = ref('正在安装插件...')
const progressText = ref('')
// 获取当前插件的标签
const pluginLabels = computed(() => {
if (!props.plugin?.plugin_label) return []
return props.plugin.plugin_label
.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0)
})
// 图片是否加载完成
const isImageLoaded = ref(false)
@@ -180,9 +190,26 @@ const dropdownItems = ref([
</VCardText>
<div class="relative flex flex-row items-start px-2 justify-between grow">
<div class="relative flex-1 min-w-0">
<VCardText class="text-white text-sm px-2 py-1 text-shadow overflow-hidden line-clamp-3 ...">
<div
class="text-white text-sm px-2 py-1 text-shadow overflow-hidden ..."
:class="{ 'line-clamp-3': !props.plugin?.plugin_label, 'line-clamp-2': props.plugin?.plugin_label }"
>
{{ props.plugin?.plugin_desc }}
</VCardText>
</div>
<!-- 插件标签 -->
<div v-if="pluginLabels.length > 0" class="plugin-app-card__tags-section px-2">
<VChip
v-for="tag in pluginLabels"
:key="tag"
size="x-small"
variant="tonal"
color="info"
class="me-1 mb-1"
tile
>
{{ tag }}
</VChip>
</div>
</div>
<div class="relative flex-shrink-0 self-center pb-3">
<VAvatar size="48">
@@ -198,9 +225,11 @@ const dropdownItems = ref([
</div>
</div>
</div>
<VCardText class="flex flex-col align-self-baseline px-2 py-2 w-full overflow-hidden">
<div class="flex flex-nowrap items-center w-full pe-7">
<span>
<VCardText
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">
<VIcon icon="mdi-github" class="me-1" />
<a
class="overflow-hidden text-ellipsis whitespace-nowrap"
@@ -210,13 +239,13 @@ const dropdownItems = ref([
>
{{ props.plugin?.plugin_author }}
</a>
</span>
<span v-if="props.count" class="ms-2 flex-shrink-0 download-count">
<VIcon icon="mdi-download" />
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
</span>
</div>
<div v-if="props.count" class="ms-2 flex-shrink-0 download-count align-middle items-center">
<VIcon size="small" icon="mdi-download" />
<span class="text-sm">{{ props.count?.toLocaleString() }}</span>
</div>
</div>
<div class="me-n3 absolute bottom-0 right-3">
<div class="absolute bottom-0 right-0">
<IconBtn>
<VIcon size="small" icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import api from '@/api'
import type { Plugin } from '@/api/types'
import { isNullOrEmptyObject } from '@core/utils'
@@ -10,7 +10,12 @@ 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'
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
@@ -54,6 +59,9 @@ const progressDialog = ref(false)
// 插件数据页面
const pluginInfoDialog = ref(false)
// 实时日志弹窗
const loggingDialog = ref(false)
// 进度框文本
const progressText = ref('正在更新插件...')
@@ -69,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,
@@ -120,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)
@@ -174,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)
@@ -205,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)
@@ -237,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([
{
@@ -257,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,
@@ -294,7 +387,7 @@ const dropdownItems = ref([
props: {
prependIcon: 'mdi-file-document-outline',
click: () => {
openLoggerWindow()
loggingDialog.value = true
},
},
},
@@ -328,7 +421,7 @@ watch(
</script>
<template>
<div>
<div class="h-full">
<!-- 插件卡片 -->
<VHover>
<template #default="hover">
@@ -358,11 +451,11 @@ watch(
</VCardText>
<div class="relative flex flex-row items-start px-2 justify-between grow">
<div class="relative flex-1 min-w-0">
<VCardText class="px-2 py-1 text-white text-sm text-shadow overflow-hidden line-clamp-3 ...">
<div class="px-2 py-1 text-white text-sm text-shadow overflow-hidden line-clamp-3 ...">
{{ props.plugin?.plugin_desc }}
</VCardText>
</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"
@@ -376,9 +469,11 @@ watch(
</div>
</div>
</div>
<VCardText class="flex flex-col align-self-baseline px-2 py-2 w-full overflow-hidden">
<div class="flex flex-nowrap items-center w-full pe-7">
<span class="author-info flex-shrink-1 overflow-hidden text-ellipsis whitespace-nowrap">
<VCardText
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">
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
<VIcon v-if="!isAvatarLoaded" size="small" icon="mdi-github" class="me-1" />
</VImg>
@@ -390,15 +485,15 @@ watch(
>
{{ props.plugin?.plugin_author }}
</a>
</span>
<span v-if="props.count" class="ms-2 flex-shrink-0 download-count">
</div>
<span v-if="props.count" class="ms-2 flex-shrink-0 download-count items-center align-middle">
<VIcon size="small" icon="mdi-download" />
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
<span class="text-sm">{{ props.count?.toLocaleString() }}</span>
</span>
</div>
<div class="me-n3 absolute bottom-0 right-3">
<div class="absolute bottom-0 right-0">
<IconBtn>
<VIcon size="small" icon="mdi-dots-vertical" />
<VIcon icon="mdi-dots-vertical" />
<VMenu v-model="menuVisible" activator="parent" close-on-content-click>
<VList>
<VListItem
@@ -448,7 +543,7 @@ watch(
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
<!-- 更新日志 -->
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" max-height="85vh" scrollable>
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable :fullscreen="!display.mdAndUp.value">
<VCard :title="t('plugin.updateHistoryTitle', { name: props.plugin?.plugin_name })">
<VDialogCloseBtn @click="releaseDialog = false" />
<VDivider />
@@ -464,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>
@@ -478,11 +711,6 @@ watch(
inset: 0;
}
.author-info {
display: flex;
align-items: center;
}
.author-avatar {
border-radius: 50%;
block-size: 24px;

View File

@@ -0,0 +1,663 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useConfirm } from '@/composables/useConfirm'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 文件夹配置接口
interface FolderConfig {
plugins?: string[]
order?: number
background?: string
icon?: string
color?: string
gradient?: string
showIcon?: boolean
}
// 输入参数
const props = defineProps({
folderName: String,
pluginCount: Number,
folderConfig: {
type: Object as PropType<FolderConfig>,
default: () => ({}),
},
width: String,
height: String,
})
// 定义触发的自定义事件
const emit = defineEmits(['open', 'delete', 'rename', 'update-config'])
// 多语言
const { t } = useI18n()
// 响应式显示
const display = useDisplay()
// 提示框
const $toast = useToast()
// 确认框
const createConfirm = useConfirm()
// 菜单显示状态
const menuVisible = ref(false)
// 重命名对话框
const renameDialog = ref(false)
// 设置对话框
const settingDialog = ref(false)
// 新名称
const newFolderName = ref('')
// 默认颜色
const defaultColor = '#2196F3'
// 默认图标
const defaultIcon = 'mdi-folder'
// 默认渐变
const defaultGradient =
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(135deg, rgba(33, 150, 243, 0.7) 0%, rgba(33, 150, 243, 0.8s) 100%)'
// 文件夹设置
const folderSettings = ref<FolderConfig>({
background: '',
icon: defaultIcon,
color: defaultColor,
gradient: defaultGradient,
showIcon: true,
})
// 计算背景图片
const backgroundImage = computed(() => {
return props.folderConfig.background || folderSettings.value.background
})
// 预设图标选项
const iconOptions = [
'mdi-folder',
'mdi-folder-star',
'mdi-folder-heart',
'mdi-folder-cog',
'mdi-folder-music',
'mdi-folder-image',
'mdi-folder-video',
'mdi-folder-download',
'mdi-folder-network',
'mdi-folder-special',
]
// 预设颜色选项
const colorOptions = [
'#2196F3', // 蓝色
'#4CAF50', // 绿色
'#FF9800', // 橙色
'#9C27B0', // 紫色
'#F44336', // 红色
'#607D8B', // 蓝灰色
'#795548', // 棕色
'#E91E63', // 粉色
]
// 预设渐变选项
const gradientOptions = [
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(33, 150, 243, 0.7) 0%, rgba(33, 150, 243, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(76, 175, 80, 0.7) 0%, rgba(76, 175, 80, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(255, 152, 0, 0.7) 0%, rgba(255, 152, 0, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(156, 39, 176, 0.7) 0%, rgba(156, 39, 176, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(244, 67, 54, 0.7) 0%, rgba(244, 67, 54, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(96, 125, 139, 0.7) 0%, rgba(96, 125, 139, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(233, 30, 99, 0.7) 0%, rgba(233, 30, 99, 0.8) 100%)',
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(63, 81, 181, 0.7) 0%, rgba(156, 39, 176, 0.8) 100%)',
]
// 计算背景渐变
const backgroundGradient = computed(() => {
const config = props.folderConfig || {}
const settings = folderSettings.value
return config.gradient || settings.gradient || gradientOptions[0]
})
// 计算图标
const folderIcon = computed(() => {
const config = props.folderConfig || {}
const settings = folderSettings.value
return config.icon || settings.icon || defaultIcon
})
// 计算图标颜色
const iconColor = computed(() => {
const config = props.folderConfig || {}
const settings = folderSettings.value
return config.color || settings.color || defaultColor
})
// 计算是否显示图标
const shouldShowIcon = computed(() => {
const config = props.folderConfig || {}
const settings = folderSettings.value
return config.showIcon !== undefined ? config.showIcon : settings.showIcon !== undefined ? settings.showIcon : true
})
// 监听props变化更新本地设置
watch(
() => props.folderConfig,
newConfig => {
if (newConfig) {
folderSettings.value = {
...folderSettings.value,
...newConfig,
}
}
},
{ deep: true, immediate: true },
)
// 打开文件夹
function openFolder() {
emit('open', props.folderName)
}
// 重命名文件夹
function showRenameDialog() {
newFolderName.value = props.folderName || ''
renameDialog.value = true
}
// 确认重命名
async function confirmRename() {
if (!newFolderName.value.trim()) {
$toast.error(t('folder.folderNameCannotBeEmpty'))
return
}
if (newFolderName.value === props.folderName) {
renameDialog.value = false
return
}
try {
emit('rename', props.folderName, newFolderName.value)
renameDialog.value = false
} catch (error) {
console.error(error)
}
}
// 删除文件夹
async function deleteFolder() {
const isConfirmed = await createConfirm({
title: t('common.confirm'),
content: t('folder.confirmDeleteFolder', { folderName: props.folderName }),
})
if (!isConfirmed) return
try {
emit('delete', props.folderName)
} catch (error) {
console.error(error)
}
}
// 显示设置对话框
function showSettingDialog() {
folderSettings.value = {
background: props.folderConfig?.background || '',
icon: props.folderConfig?.icon || defaultIcon,
color: props.folderConfig?.color || defaultColor,
gradient: props.folderConfig?.gradient || gradientOptions[0],
showIcon: props.folderConfig?.showIcon !== undefined ? props.folderConfig.showIcon : true,
}
settingDialog.value = true
}
// 保存设置
function saveSettings() {
const config = {
...props.folderConfig,
...folderSettings.value,
}
emit('update-config', props.folderName, config)
settingDialog.value = false
$toast.success(t('folder.folderSettingsSaved'))
}
// 弹出菜单
const dropdownItems = ref([
{
title: t('folder.settingAppearance'),
value: 0,
show: true,
props: {
prependIcon: 'mdi-palette',
click: showSettingDialog,
},
},
{
title: t('folder.rename'),
value: 1,
show: true,
props: {
prependIcon: 'mdi-pencil',
click: showRenameDialog,
},
},
{
title: t('folder.deleteFolder'),
value: 2,
show: true,
props: {
prependIcon: 'mdi-delete',
color: 'error',
click: deleteFolder,
},
},
])
</script>
<template>
<div class="h-full">
<!-- 文件夹卡片 -->
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:ripple="false"
:width="props.width"
:height="props.height"
min-height="8.5rem"
@click="openFolder"
class="plugin-folder-card h-full"
:class="{
'plugin-folder-card--mobile': display.mobile,
'plugin-folder-card--hover': hover.isHovering,
}"
>
<template v-if="backgroundImage" #image>
<VImg :src="backgroundImage" cover position="top"> </VImg>
</template>
<!-- 背景遮罩当有背景图片时 -->
<div v-if="backgroundImage" class="plugin-folder-card__overlay" />
<!-- 背景渐变层 -->
<div v-else class="plugin-folder-card__bg" :style="{ background: backgroundGradient }" />
<!-- 卡片内容 -->
<div class="plugin-folder-card__content">
<!-- 主体内容 -->
<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"
:color="iconColor"
:class="{ 'cursor-move': display.mdAndUp.value }"
/>
</div>
<!-- 文件夹信息 -->
<div
class="plugin-folder-card__info"
:class="{ 'cursor-move': display.mdAndUp.value, 'plugin-folder-card__info--no-icon': !shouldShowIcon }"
>
<!-- 文件夹名称 -->
<h3 class="plugin-folder-card__name">
{{ props.folderName }}
</h3>
<!-- 插件数量 -->
<p class="plugin-folder-card__count">{{ t('folder.pluginCount', { count: props.pluginCount }) }}</p>
</div>
</div>
<!-- 更多菜单按钮 - 右下角 -->
<div class="absolute top-0 right-0">
<VMenu v-model="menuVisible" location="top end" :close-on-content-click="true">
<template #activator="{ props: menuProps }">
<IconBtn v-bind="menuProps" @click.stop>
<VIcon size="small" icon="mdi-dots-vertical" class="text-white" />
</IconBtn>
</template>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
:base-color="item.props.color"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" size="16" />
</template>
<VListItemTitle class="text-body-2">{{ item.title }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</div>
</div>
</VCard>
</template>
</VHover>
<!-- 重命名对话框 -->
<VDialog v-if="renameDialog" v-model="renameDialog" max-width="400">
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-pencil" class="me-2" />
</template>
<VCardTitle>{{ t('folder.renameFolder') }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="renameDialog = false" />
<VDivider />
<VCardText>
<VTextField
v-model="newFolderName"
:label="t('folder.folderName')"
variant="outlined"
autofocus
@keyup.enter="confirmRename"
/>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn color="primary" prepend-icon="mdi-check" class="px-5" @click="confirmRename">确认</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 设置对话框 -->
<VDialog
v-if="settingDialog"
v-model="settingDialog"
max-width="600"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VDialogCloseBtn @click="settingDialog = false" />
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-palette" class="mr-2" />
{{ t('folder.folderAppearanceSettings') }}
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<!-- 显示图标开关 -->
<VCol cols="12">
<VSwitch
v-model="folderSettings.showIcon"
:label="t('folder.showFolderIcon')"
color="primary"
hide-details
/>
</VCol>
<!-- 图标选择 -->
<VCol v-if="folderSettings.showIcon" cols="12" md="6">
<VCardSubtitle class="pa-0 mb-2">{{ t('folder.icon') }}</VCardSubtitle>
<div class="icon-grid">
<VBtn
v-for="icon in iconOptions"
icon
:key="icon"
:variant="folderSettings.icon === icon ? 'tonal' : 'text'"
:color="folderSettings.icon === icon ? 'primary' : 'default'"
size="large"
class="ma-1"
@click="folderSettings.icon = icon"
>
<VIcon :icon="icon" size="24" />
</VBtn>
</div>
</VCol>
<!-- 颜色选择 -->
<VCol v-if="folderSettings.showIcon" cols="12" md="6">
<VCardSubtitle class="pa-0 mb-2">{{ t('folder.iconColor') }}</VCardSubtitle>
<div class="color-grid">
<VBtn
v-for="color in colorOptions"
:key="color"
:variant="folderSettings.color === color ? 'tonal' : 'text'"
:color="color"
size="large"
class="ma-1 color-btn"
:style="{ backgroundColor: color }"
@click="folderSettings.color = color"
>
<VIcon v-if="folderSettings.color === color" icon="mdi-check" color="white" />
</VBtn>
</div>
</VCol>
<!-- 渐变背景选择 -->
<VCol cols="12">
<VCardSubtitle class="pa-0 mb-2">{{ t('folder.backgroundGradient') }}</VCardSubtitle>
<div class="gradient-grid">
<VBtn
v-for="(gradient, index) in gradientOptions"
:key="index"
:variant="folderSettings.gradient === gradient ? 'tonal' : 'text'"
class="ma-1 gradient-btn"
:style="{ background: gradient }"
size="large"
@click="folderSettings.gradient = gradient"
>
<VIcon v-if="folderSettings.gradient === gradient" icon="mdi-check" color="white" />
</VBtn>
</div>
</VCol>
<!-- 自定义背景图片 -->
<VCol cols="12">
<VTextField
v-model="folderSettings.background"
:label="t('folder.customBackgroundImageURL')"
placeholder="https://example.com/image.jpg"
variant="outlined"
:hint="t('folder.customBackgroundImageHint')"
persistent-hint
prepend-inner-icon="mdi-image"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn color="primary" prepend-icon="mdi-content-save" class="px-5" @click="saveSettings">保存</VBtn>
</VCardActions>
</VCard>
</VDialog>
</div>
</template>
<style lang="scss" scoped>
.plugin-folder-card {
position: relative;
overflow: hidden;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&--hover {
transform: translateY(-4px);
}
&__bg {
position: absolute;
z-index: 0;
inset: 0;
outline: none;
}
&__overlay {
position: absolute;
z-index: 1;
background: rgba(0, 0, 0, 60%);
inset: 0;
}
&__content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
padding: 16px;
block-size: 100%;
padding-block-end: 12px;
.plugin-folder-card--mobile & {
padding: 12px;
padding-block-end: 10px;
}
}
&__body {
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 16px;
padding-block: 0;
padding-inline: 8px;
.plugin-folder-card--mobile & {
gap: 12px;
padding-block: 0;
padding-inline: 4px;
}
&--no-icon {
align-items: flex-start;
justify-content: flex-start;
padding: 16px;
gap: 0;
.plugin-folder-card--mobile & {
padding: 12px;
gap: 0;
}
}
}
&__icon-container {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
}
&__info {
flex: 1;
min-block-size: 0;
text-align: start;
&--no-icon {
flex: none;
text-align: start;
}
}
&__name {
display: -webkit-box;
overflow: hidden;
margin: 0;
-webkit-box-orient: vertical;
color: white;
font-size: 1.1rem;
font-weight: 600;
-webkit-line-clamp: 1;
line-clamp: 1;
line-height: 1.3;
max-inline-size: none;
text-overflow: ellipsis;
text-shadow: 0 2px 4px rgba(0, 0, 0, 50%);
.plugin-folder-card--mobile & {
font-size: 1rem;
}
.plugin-folder-card__info--no-icon & {
font-size: 1.3rem;
font-weight: 700;
-webkit-line-clamp: 2;
line-clamp: 2;
margin-block-end: 4px;
.plugin-folder-card--mobile & {
font-size: 1.2rem;
}
}
}
&__count {
color: white;
font-size: 0.85rem;
margin-block: 2px 0;
margin-inline: 0;
opacity: 0.9;
text-shadow: 0 1px 2px rgba(0, 0, 0, 50%);
.plugin-folder-card--mobile & {
font-size: 0.8rem;
}
.plugin-folder-card__info--no-icon & {
font-size: 0.9rem;
margin-block-start: 0;
.plugin-folder-card--mobile & {
font-size: 0.85rem;
}
}
}
}
// 设置对话框样式
.icon-grid {
display: grid;
gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
max-block-size: 200px;
overflow-y: auto;
}
.color-grid {
display: grid;
gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
}
.gradient-grid {
display: grid;
gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
max-block-size: 200px;
overflow-y: auto;
}
.color-btn {
border-radius: 8px !important;
block-size: 60px !important;
min-inline-size: 60px !important;
}
.gradient-btn {
border-radius: 8px !important;
block-size: 60px !important;
min-inline-size: 120px !important;
}
</style>

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

@@ -11,7 +11,11 @@ import api from '@/api'
import type { Site, SiteStatistic, SiteUserData } from '@/api/types'
import { isNullOrEmptyObject } from '@/@core/utils'
import { formatFileSize } from '@/@core/utils/formatters'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -224,7 +228,7 @@ onMounted(() => {
<!-- 顶部图标和站点名称 -->
<div class="flex items-center mb-1">
<!-- 站点图标 -->
<VAvatar tile rounded="lg" size="32" class="me-2 cursor-move">
<VAvatar tile rounded="lg" size="32" class="me-2" :class="{ 'cursor-move': display.mdAndUp.value }">
<VImg :src="siteIcon" class="w-full h-full" :alt="cardProps.site?.name" cover>
<template #placeholder>
<div class="w-full h-full">

View File

@@ -16,6 +16,10 @@ import { useToast } from 'vue-toast-notification'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useI18n } from 'vue-i18n'
import { storageIconDict } from '@/api/constants'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -200,9 +204,18 @@ function onClose() {
@close="aListConfigDialog = false"
@done="handleDone"
/>
<VDialog v-if="customConfigDialog" v-model="customConfigDialog" scrollable max-width="30rem">
<VDialog
v-if="customConfigDialog"
v-model="customConfigDialog"
scrollable
max-width="30rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-cog" />
</template>
<VCardTitle>{{ t('storage.custom') }}</VCardTitle>
<VDialogCloseBtn v-model="customConfigDialog" />
</VCardItem>
@@ -215,16 +228,21 @@ 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>
<VCardActions class="pt-3">
<VBtn @click="handleDone" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
<VBtn @click="handleDone" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import SubscribeFilesDialog from '../dialog/SubscribeFilesDialog.vue'
import SubscribeShareDialog from '../dialog/SubscribeShareDialog.vue'
@@ -9,6 +9,10 @@ import api from '@/api'
import type { Subscribe } from '@/api/types'
import router from '@/router'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -348,7 +352,11 @@ function onSubscribeEditRemove() {
</template>
<div>
<VCardText class="flex items-center pt-3 pb-2">
<div class="h-auto w-14 flex-shrink-0 overflow-hidden rounded-md cursor-move" v-if="imageLoaded">
<div
class="h-auto w-14 flex-shrink-0 overflow-hidden rounded-md"
v-if="imageLoaded"
:class="{ 'cursor-move': display.mdAndUp.value }"
>
<VImg :src="posterUrl" aspect-ratio="2/3" cover>
<template #placeholder>
<div class="w-full h-full">

View File

@@ -4,7 +4,7 @@ import { Subscribe, User } from '@/api/types'
import { useUserStore } from '@/stores'
import avatar1 from '@images/avatars/avatar-1.png'
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { Workflow } from '@/api/types'
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import WorkflowAddEditDialog from '@/components/dialog/WorkflowAddEditDialog.vue'
import WorkflowActionsDialog from '@/components/dialog/WorkflowActionsDialog.vue'
import api from '@/api'

View File

@@ -134,69 +134,75 @@ onMounted(() => {
<template>
<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

@@ -1,6 +1,10 @@
<script lang="ts" setup>
import api from '@/api'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -66,9 +70,18 @@ async function savaAlistConfig() {
</script>
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VCard :title="t('dialog.alistConfig.title')">
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardItem>
<template #prepend>
<VIcon icon="mdi-cog-outline" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.alistConfig.title') }}
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
@@ -77,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">
@@ -86,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'">
@@ -94,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'">
@@ -103,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'">
@@ -111,16 +128,17 @@ async function savaAlistConfig() {
:hint="t('dialog.alistConfig.loginTypeOptions.token')"
:label="t('dialog.alistConfig.loginTypeOptions.token')"
persistent-hint
prepend-inner-icon="mdi-key"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.alistConfig.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
<VSpacer />
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('dialog.alistConfig.complete') }}
</VBtn>
</VCardActions>

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup>
import api from '@/api'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -106,11 +110,20 @@ onUnmounted(() => {
</script>
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard :title="t('dialog.aliyunAuth.loginTitle')">
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 rounded text-center p-3 border">
<VCardItem>
<template #prepend>
<VIcon icon="mdi-qrcode" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.aliyunAuth.loginTitle') }}
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText class="pt-2 flex flex-col items-center justify-center">
<div class="mt-6 rounded text-center p-3 border">
<VImg class="mx-auto" :src="qrCodeUrl" width="200" height="200">
<template #placeholder>
<div class="w-full h-full">
@@ -119,16 +132,18 @@ onUnmounted(() => {
</template>
</VImg>
</div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />
</VAlert>
<div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />
</VAlert>
</div>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.aliyunAuth.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
<VSpacer />
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('dialog.aliyunAuth.complete') }}
</VBtn>
</VCardActions>

View File

@@ -25,14 +25,20 @@ 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 />
<VBtn variant="elevated" @click="handleImport" prepend-icon="mdi-import" class="px-5 me-3">
<VBtn @click="handleImport" prepend-icon="mdi-import" class="px-5 me-3">
{{ t('dialog.importCode.import') }}
</VBtn>
</VCardActions>

View File

@@ -161,18 +161,12 @@ onBeforeMount(async () => {
</div>
</VCardText>
<VCardActions class="pt-3">
<VBtn v-if="props.plugin?.has_page" @click="emit('switch')" variant="outlined" color="info">
<VBtn v-if="props.plugin?.has_page" @click="emit('switch')" color="info">
{{ t('dialog.pluginConfig.viewData') }}
</VBtn>
<VSpacer />
<!-- 只有Vuetify模式显示默认保存按钮Vue模式由组件内部控制 -->
<VBtn
v-if="renderMode === 'vuetify'"
@click="savePluginConf"
variant="elevated"
prepend-icon="mdi-content-save"
class="px-5"
>
<VBtn v-if="renderMode === 'vuetify'" @click="savePluginConf" prepend-icon="mdi-content-save" class="px-5">
保存
</VBtn>
</VCardActions>

View File

@@ -2,6 +2,11 @@
import api from '@/api'
import { useToast } from 'vue-toast-notification'
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -9,6 +14,16 @@ const $toast = useToast()
// 插件仓库设置字符串
const repoString = ref('')
// 用于显示的仓库地址数组
const repoArray = ref<string[]>([])
// 计算属性:在数组和换行符分隔的字符串之间转换
const displayRepos = computed({
get: () => repoArray.value.join('\n'),
set: (value: string) => {
repoArray.value = value.split('\n').filter((repo: string) => repo.trim() !== '')
},
})
// 定义事件
const emit = defineEmits(['save', 'close'])
@@ -17,7 +32,10 @@ const emit = defineEmits(['save', 'close'])
async function queryMarketRepoSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/PLUGIN_MARKET')
if (result && result.data && result.data.value) repoString.value = result.data.value
if (result && result.data && result.data.value) {
repoString.value = result.data.value
repoArray.value = result.data.value.split(',').filter((repo: string) => repo.trim() !== '')
}
} catch (error) {
console.log(error)
}
@@ -26,8 +44,9 @@ async function queryMarketRepoSetting() {
// 保存设置
async function saveHandle() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET', repoString.value)
// 将数组转换为逗号分隔的字符串
const repoStringToSave = repoArray.value.join(',')
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET', repoStringToSave)
if (result.success) {
$toast.success(t('dialog.pluginMarketSetting.saveSuccess'))
@@ -44,7 +63,7 @@ onMounted(() => {
</script>
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VCardItem>
<VCardTitle>
@@ -53,17 +72,19 @@ onMounted(() => {
</VCardTitle>
<VDialogCloseBtn @click="emit('close')" />
</VCardItem>
<VDivider />
<VCardText class="pt-2">
<VTextarea
v-model="repoString"
v-model="displayRepos"
:placeholder="t('dialog.pluginMarketSetting.repoPlaceholder')"
:hint="t('dialog.pluginMarketSetting.repoHint')"
persistent-hint
auto-grow
/>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="saveHandle" prepend-icon="mdi-content-save-check" class="px-5 me-3">
<VBtn @click="saveHandle" prepend-icon="mdi-content-save-check" class="px-5 me-3">
{{ t('dialog.pluginMarketSetting.save') }}
</VBtn>
</VCardActions>

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup>
import api from '@/api'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -53,32 +57,44 @@ async function handleReset() {
</script>
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VCard :title="t('dialog.rcloneConfig.title')">
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardItem>
<template #prepend>
<VIcon icon="mdi-cog-outline" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.rcloneConfig.title') }}
</VCardTitle>
</VCardItem>
<VDivider />
<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
v-model:value="props.conf.content"
lang="ini"
theme="monokai"
style="block-size: 30rem"
class="rounded"
class="rounded h-full min-h-[30rem]"
>
</VAceEditor>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.rcloneConfig.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
<VSpacer />
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('dialog.rcloneConfig.complete') }}
</VBtn>
</VCardActions>

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>
@@ -438,10 +459,10 @@ onUnmounted(() => {
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn variant="elevated" color="success" @click="transfer(true)" prepend-icon="mdi-plus" class="px-5">
<VBtn color="success" @click="transfer(true)" prepend-icon="mdi-plus" class="px-5">
{{ t('dialog.reorganize.addToQueue') }}
</VBtn>
<VBtn variant="elevated" @click="transfer(false)" prepend-icon="mdi-arrow-right-bold" class="px-5">
<VBtn @click="transfer(false)" prepend-icon="mdi-arrow-right-bold" class="px-5">
{{ t('dialog.reorganize.reorganizeNow') }}
</VBtn>
</VCardActions>

View File

@@ -6,6 +6,10 @@ import { NavMenu } from '@/@layouts/types'
import { useUserStore } from '@/stores'
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -302,29 +306,24 @@ onMounted(() => {
})
</script>
<template>
<VDialog v-model="dialog" max-width="42rem" scrollable maxHeight="85vh">
<VDialog v-model="dialog" max-width="42rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard class="search-dialog">
<!-- 搜索输入框 -->
<VCardItem class="pa-4 pa-sm-5 search-box-container">
<template #prepend>
<VIcon icon="mdi-magnify" color="primary" size="x-large" />
</template>
<VCombobox
ref="searchWordInput"
v-model="searchWord"
density="comfortable"
variant="outlined"
class="search-input"
prepend-inner-icon="mdi-magnify"
append-inner-icon="mdi-close"
@click:append-inner="emit('close')"
:placeholder="t('dialog.searchBar.searchPlaceholder')"
@keydown.enter="searchMedia('media')"
hide-details
clearable
/>
<template #append>
<IconBtn>
<VIcon icon="mdi-close" color="primary" @click="emit('close')" size="x-large" />
</IconBtn>
</template>
</VCardItem>
<VDivider />

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">
@@ -163,27 +156,16 @@ const filteredSites = computed(() => {
</div>
</VCardText>
<VDivider class="search-divider" />
<VCardActions class="pa-4">
<VCardActions class="pt-3">
<VSpacer />
<VBtn
color="grey-darken-1"
variant="text"
@click="emit('close')"
class="mr-2 d-flex align-center justify-center"
>
{{ t('dialog.searchSite.cancel') }}
</VBtn>
<VBtn
color="primary"
variant="flat"
:disabled="selectedSites.length === 0"
@click="emit('search', selectedSites)"
prepend-icon="mdi-magnify"
class="d-flex align-center justify-center px-5"
>
{{ t('dialog.searchSite.confirm') }}
{{ t('common.search') }}
</VBtn>
</VCardActions>
</VCard>

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>
@@ -326,24 +342,10 @@ onMounted(async () => {
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn
v-if="props.oper === 'add'"
color="primary"
variant="elevated"
@click="addSite"
prepend-icon="mdi-plus"
class="px-5"
>
<VBtn v-if="props.oper === 'add'" color="primary" @click="addSite" prepend-icon="mdi-plus" class="px-5">
{{ t('site.actions.add') }}
</VBtn>
<VBtn
v-else
color="primary"
variant="elevated"
@click="updateSiteInfo"
prepend-icon="mdi-content-save"
class="px-5"
>
<VBtn v-else color="primary" @click="updateSiteInfo" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>

View File

@@ -71,7 +71,7 @@ async function updateSiteCookie() {
}
</script>
<template>
<VDialog max-width="30rem">
<VDialog max-width="30rem" scrollable>
<!-- Dialog Content -->
<VCard :title="t('dialog.siteCookieUpdate.title')">
<VDialogCloseBtn @click="emit('close')" />
@@ -102,7 +102,6 @@ async function updateSiteCookie() {
<VCardActions class="mx-auto">
<VBtn
size="large"
variant="elevated"
@click="updateSiteCookie"
:disabled="updateButtonDisable"
:loading="updateButtonDisable"

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>
@@ -186,7 +190,6 @@ onMounted(() => {
fixed-header
hover
:items-per-page-text="t('dialog.siteResource.itemsPerPage')"
:page-text="t('dialog.siteResource.pageText')"
:loading-text="t('dialog.siteResource.loading')"
class="h-full"
>

View File

@@ -4,7 +4,7 @@ import { numberValidator } from '@/@validators'
import api from '@/api'
import type { DownloaderConf, FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
import { useDisplay } from 'vuetify'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import { useI18n } from 'vue-i18n'
import { qualityOptions, resolutionOptions, effectOptions } from '@/api/constants'
// i18n
@@ -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>
@@ -504,12 +526,11 @@ onMounted(() => {
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VBtn v-if="!props.default" color="error" @click="removeSubscribe" variant="outlined" class="me-3">
<VBtn v-if="!props.default" color="error" @click="removeSubscribe" class="me-3">
{{ t('dialog.subscribeEdit.cancelSubscribe') }}
</VBtn>
<VSpacer />
<VBtn
variant="elevated"
@click=";`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`"
prepend-icon="mdi-content-save"
class="px-5"

View File

@@ -56,11 +56,17 @@ 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>
<VCardText>
<VDialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}" class="pt-2">
@@ -72,6 +78,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 +88,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 +98,7 @@ const $toast = useToast()
:rules="[requiredValidator]"
:hint="t('dialog.subscribeShare.shareUserHint')"
persistent-hint
prepend-inner-icon="mdi-account-outline"
/>
</VCol>
</VRow>
@@ -97,14 +106,7 @@ const $toast = useToast()
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn
variant="elevated"
:disabled="shareDoing"
@click="doShare"
prepend-icon="mdi-share"
class="px-5"
:loading="shareDoing"
>
<VBtn :disabled="shareDoing" @click="doShare" prepend-icon="mdi-share" class="px-5" :loading="shareDoing">
{{ t('dialog.subscribeShare.confirmShare') }}
</VBtn>
</VCardActions>

View File

@@ -2,6 +2,10 @@
import api from '@/api'
import QrcodeVue from 'qrcode.vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -111,23 +115,34 @@ onUnmounted(() => {
</script>
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard :title="t('dialog.u115Auth.loginTitle')">
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VDialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 rounded text-center p-3 border">
<VCardItem>
<template #prepend>
<VIcon icon="mdi-qrcode" class="me-2" />
</template>
<VCardTitle>
{{ t('dialog.u115Auth.loginTitle') }}
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText class="pt-2 flex flex-col items-center justify-center">
<div class="mt-6 rounded text-center p-3 border">
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" />
</div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />
</VAlert>
<div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />
</VAlert>
</div>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.u115Auth.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
<VSpacer />
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('dialog.u115Auth.complete') }}
</VBtn>
</VCardActions>

View File

@@ -290,12 +290,15 @@ onMounted(() => {
</script>
<template>
<VDialog scrollable :close-on-back="false" eager max-width="40rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.oper === 'add' ? t('dialog.userAddEdit.add') : t('dialog.userAddEdit.edit')}${
props.oper !== 'add' ? ` - ${userName}` : ''
}`"
>
<VDialog scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
<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>
@@ -461,7 +481,6 @@ onMounted(() => {
v-if="props.oper === 'add'"
:disabled="isAdding"
color="primary"
variant="elevated"
@click="addUser"
prepend-icon="mdi-plus"
class="px-5"
@@ -473,7 +492,6 @@ onMounted(() => {
v-else
:disabled="isUpdating"
color="primary"
variant="elevated"
@click="updateUser"
prepend-icon="mdi-content-save"
class="px-5"

View File

@@ -3,6 +3,10 @@ import { isNullOrEmptyObject } from '@/@core/utils'
import api from '@/api'
import { useToast } from 'vue-toast-notification'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持
const { t } = useI18n()
@@ -133,9 +137,16 @@ onMounted(async () => {
</script>
<template>
<VDialog width="40rem" max-height="85vh">
<VCard :title="t('dialog.userAuth.title')">
<VDialogCloseBtn @click="emit('close')" />
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
<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">
@@ -146,6 +157,7 @@ onMounted(async () => {
item-title="name"
:label="t('dialog.userAuth.selectSite')"
item-props
prepend-inner-icon="mdi-web"
>
</VSelect>
</VCol>
@@ -165,14 +177,7 @@ onMounted(async () => {
</VRow>
</VCardText>
<VCardText class="text-center">
<VBtn
variant="elevated"
@click="handleDone"
prepend-icon="mdi-check"
class="px-5"
size="large"
:disabled="loading"
>
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5" size="large" :disabled="loading">
{{ t('dialog.userAuth.authBtn') }}
</VBtn>
</VCardText>

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>
@@ -123,18 +132,10 @@ async function editWorkflow() {
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn
v-if="workflow"
block
color="primary"
variant="elevated"
@click="editWorkflow"
prepend-icon="mdi-content-save"
class="px-5"
>
<VBtn v-if="workflow" color="primary" @click="editWorkflow" prepend-icon="mdi-content-save" class="px-5">
{{ t('dialog.workflowAddEdit.confirm') }}
</VBtn>
<VBtn v-else block color="primary" variant="elevated" @click="addWorkflow" prepend-icon="mdi-plus" class="px-5">
<VBtn v-else color="primary" @click="addWorkflow" prepend-icon="mdi-plus" class="px-5">
{{ t('dialog.workflowAddEdit.confirm') }}
</VBtn>
</VCardActions>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { AxiosRequestConfig } from 'axios'
import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog'
import { useConfirm } from '@/composables/useConfirm'
import { useToast } from 'vue-toast-notification'
import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
import { formatBytes } from '@core/utils/formatters'
@@ -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')" />
@@ -710,10 +721,10 @@ onMounted(() => {
</VRow>
</VCardText>
<VCardActions>
<VBtn color="success" variant="elevated" @click="get_recommend_name" prepend-icon="mdi-magic" class="px-5 me-3">
<VBtn color="success" @click="get_recommend_name" prepend-icon="mdi-magic" class="px-5 me-3">
{{ t('file.autoRecognizeName') }}
</VBtn>
<VBtn :disabled="!newName" variant="elevated" @click="rename" prepend-icon="mdi-check" class="px-5 me-3">
<VBtn :disabled="!newName" @click="rename" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('common.confirm') }}
</VBtn>
</VCardActions>

View File

@@ -165,21 +165,28 @@ 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" />
<VBtn :disabled="!newFolderName" variant="elevated" @click="mkdir" prepend-icon="mdi-check" class="px-5 me-3">
<VBtn :disabled="!newFolderName" @click="mkdir" prepend-icon="mdi-folder-plus" class="px-5 me-3">
{{ t('common.create') }}
</VBtn>
</VCardActions>

View File

@@ -0,0 +1,88 @@
import { ref } from 'vue'
import { createApp } from 'vue'
import i18n from '@/plugins/i18n'
import vuetify from '@/plugins/vuetify'
import ConfirmDialog from '@/@core/components/ConfirmDialog.vue'
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
interface ConfirmOptions {
type?: 'info' | 'warn' | 'error'
title?: string
content?: string
confirmText?: string
cancelText?: string
width?: string | number
}
let resolvePromise: ((value: boolean) => void) | null = null
// 创建确认对话框实例
async function createConfirmDialog(options: ConfirmOptions = {}) {
return new Promise<boolean>(resolve => {
resolvePromise = resolve
// 创建容器
const container = document.createElement('div')
document.body.appendChild(container)
// 处理国际化
const i18nOptions = {
...options,
title: options.title || i18n.global.t('common.confirm'),
confirmText: options.confirmText || i18n.global.t('common.confirm'),
cancelText: options.cancelText || i18n.global.t('common.cancel'),
}
// 创建应用实例
const app = createApp(ConfirmDialog, {
modelValue: true,
...i18nOptions,
'onUpdate:modelValue': (val: boolean) => {
if (!val) {
cleanup()
}
},
onConfirm: () => {
resolvePromise?.(true)
cleanup()
},
onCancel: () => {
resolvePromise?.(false)
cleanup()
},
})
// 注册必要的组件
app.component('VDialogCloseBtn', DialogCloseBtn)
// 使用插件
app.use(vuetify)
app.use(i18n)
// 挂载应用
app.mount(container)
// 清理函数
const cleanup = () => {
app.unmount()
document.body.removeChild(container)
}
})
}
// 创建一个函数对象,同时支持直接调用和解构
const confirmFunction = Object.assign(createConfirmDialog, {
createConfirm: createConfirmDialog,
})
// 导出 useConfirm 函数
export function useConfirm() {
return confirmFunction
}
// 插件
export default {
install: (app: any) => {
app.provide('confirm', { createConfirm: createConfirmDialog })
},
}

View File

@@ -203,7 +203,13 @@ onMounted(() => {
</VCard>
</VMenu>
<!-- 名称测试弹窗 -->
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" max-width="45rem" scrollable>
<VDialog
v-if="nameTestDialog"
v-model="nameTestDialog"
max-width="45rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VCardTitle>
@@ -219,7 +225,13 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 网络测试弹窗 -->
<VDialog v-if="netTestDialog" v-model="netTestDialog" max-width="35rem" max-height="85vh" scrollable>
<VDialog
v-if="netTestDialog"
v-model="netTestDialog"
max-width="35rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VCardTitle>
@@ -258,12 +270,18 @@ onMounted(() => {
</VCardItem>
<VDivider />
<VCardText>
<LoggingView />
<LoggingView logfile="moviepilot.log" />
</VCardText>
</VCard>
</VDialog>
<!-- 过滤规则弹窗 -->
<VDialog v-if="ruleTestDialog" v-model="ruleTestDialog" max-width="35rem" scrollable>
<VDialog
v-if="ruleTestDialog"
v-model="ruleTestDialog"
max-width="35rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VCardTitle>
@@ -279,7 +297,13 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 系统健康检查弹窗 -->
<VDialog v-if="systemTestDialog" v-model="systemTestDialog" max-width="35rem" scrollable>
<VDialog
v-if="systemTestDialog"
v-model="systemTestDialog"
max-width="35rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VCardTitle>

View File

@@ -13,6 +13,7 @@ import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
import { saveLocalTheme } from '@/@core/utils/theme'
import type { ThemeSwitcherTheme } from '@layouts/types'
import { useConfirm } from '@/composables/useConfirm'
// 认证 Store
const authStore = useAuthStore()
@@ -32,9 +33,6 @@ const progressDialog = ref(false)
// 站点认证对话框
const siteAuthDialog = ref(false)
// 重启确认对话框
const restartDialog = ref(false)
// 自定义CSS弹窗
const cssDialog = ref(false)
@@ -47,6 +45,9 @@ const showLanguageMenu = ref(false)
// 自定义CSS
const customCSS = ref('')
// 确认框
const { createConfirm } = useConfirm()
// 执行注销操作
function logout() {
// 清除登录状态信息
@@ -57,7 +58,6 @@ function logout() {
// 执行重启操作
async function restart() {
restartDialog.value = false
// 调用API重启
try {
// 显示等待框
@@ -79,7 +79,15 @@ async function restart() {
// 显示重启确认对话框
async function showRestartDialog() {
restartDialog.value = true
const isConfirmed = await createConfirm({
type: 'warn',
title: t('app.confirmRestart'),
content: t('app.restartTip'),
})
if (!isConfirmed) return
await restart()
}
// 显示站点认证对话框
@@ -417,32 +425,6 @@ onMounted(() => {
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('app.restarting')" />
<!-- 用户认证对话框 -->
<UserAuthDialog v-if="siteAuthDialog" v-model="siteAuthDialog" @done="siteAuthDone" @close="siteAuthDialog = false" />
<!-- 重启确认对话框 -->
<VDialog v-if="restartDialog" v-model="restartDialog" max-width="25rem">
<VCard>
<VCardItem>
<div class="d-flex align-center justify-center mt-3">
<VAvatar color="warning" variant="text" size="x-large">
<VIcon size="x-large" icon="mdi-alert" />
</VAvatar>
<div class="ms-3">
<p class="font-weight-bold text-xl text-high-emphasis">{{ t('app.confirmRestart') }}</p>
<p>{{ t('app.restartTip') }}</p>
</div>
</div>
</VCardItem>
<VCardActions class="mx-auto">
<VBtn variant="tonal" color="secondary" class="px-5" @click="restartDialog = false">{{
t('common.cancel')
}}</VBtn>
<VBtn variant="elevated" color="error" @click="restart" prepend-icon="mdi-restart" class="px-5">{{
t('common.confirm')
}}</VBtn>
</VCardActions>
<VDialogCloseBtn @click="restartDialog = false" />
</VCard>
</VDialog>
<!-- 自定义 CSS -->
<VDialog v-if="cssDialog" v-model="cssDialog" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>

View File

@@ -39,6 +39,7 @@ export default {
unsubscribe: 'Unsubscribe',
media: 'Media',
unknown: 'Unknown',
notice: 'Notice',
},
mediaType: {
movie: 'Movie',
@@ -852,8 +853,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,6 +1096,12 @@ 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',
@@ -1637,7 +1644,7 @@ export default {
title: 'Plugin Market Settings',
repoUrl: 'Plugin Repository URL',
repoPlaceholder: 'Format: https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
repoHint: 'Multiple URLs separated by commas, only Github repositories are supported',
repoHint: 'Multiple URLs separated by lines, only Github repositories are supported',
close: 'Close',
save: 'Save',
saveSuccess: 'Plugin repository saved successfully',
@@ -1703,8 +1710,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 +1760,7 @@ export default {
},
subscribeEdit: {
titleDefault: 'Default Subscription Rules',
titleEditFormat: 'Edit Subscription - {name} {season}',
titleEdit: 'Edit Subscription',
seasonFormat: 'Season {number}',
tabs: {
basic: 'Basic',
@@ -1866,6 +1873,7 @@ export default {
peersColumn: 'Peers',
viewDetails: 'View Details',
downloadTorrent: 'Download Torrent',
pageText: '{0}-{1} of {2} items',
},
forkSubscribe: {
title: 'Copy Subscription',
@@ -1998,6 +2006,54 @@ export default {
updateHistoryTitle: '{name} Update History',
updateToLatest: 'Update to Latest Version',
updatingTo: 'Updating {name} to v{version} ...',
folderNameEmpty: 'Folder name cannot be empty',
folderExists: 'Folder already exists',
folderCreateSuccess: 'Folder created successfully',
folderRenameSuccess: 'Folder renamed successfully',
folderRenameFailed: 'Failed to rename folder',
folderDeleteSuccess: 'Folder deleted successfully',
folderDeleteFailed: 'Failed to delete folder',
removeFromFolderSuccess: 'Plugin removed from folder',
operationFailed: 'Operation failed',
saveFolderConfigFailed: 'Failed to save folder config',
newFolder: 'New Folder',
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',
@@ -2380,4 +2436,23 @@ export default {
required: 'This field is required',
number: 'Please enter a number',
},
folder: {
settingAppearance: 'Appearance Settings',
rename: 'Rename',
deleteFolder: 'Delete Folder',
folderNameCannotBeEmpty: 'Folder name cannot be empty',
confirmDeleteFolder:
'Are you sure you want to delete folder "{folderName}"? Plugins in this folder will be moved back to the main list.',
folderSettingsSaved: 'Folder settings saved',
renameFolder: 'Rename Folder',
folderName: 'Folder Name',
folderAppearanceSettings: 'Folder Appearance Settings',
showFolderIcon: 'Show Folder Icon',
icon: 'Icon',
iconColor: 'Icon Color',
backgroundGradient: 'Background Gradient',
customBackgroundImageURL: 'Custom Background Image URL (Optional)',
customBackgroundImageHint: 'Supports web image URLs, leave blank for gradient background',
pluginCount: '{count} Plugins',
},
}

View File

@@ -39,6 +39,7 @@ export default {
unsubscribe: '取消订阅',
media: '媒体',
unknown: '未知',
notice: '注意',
},
mediaType: {
movie: '电影',
@@ -849,8 +850,8 @@ export default {
browserSimulation: '使用浏览器模拟真实访问该站点',
},
actions: {
add: '新增',
edit: '编辑',
add: '新增站点',
edit: '编辑站点',
},
messages: {
addSuccess: '新增站点成功',
@@ -1085,6 +1086,12 @@ export default {
securityImageDomainsHint: '允许缓存的图片域名白名单,用于控制可信任的图片来源',
noSecurityImageDomains: '暂无安全域名',
securityImageDomainAdd: '添加域名image.tmdb.org',
proxyHost: '代理服务器',
proxyHostHint: '设置代理服务器地址支持http(s)、socks5、socks5h 等协议',
moviePilotAutoUpdate: '自动更新MoviePilot',
moviePilotAutoUpdateHint: '重启时自动更新MoviePilot到最新发行版本',
autoUpdateResource: '自动更新站点资源',
autoUpdateResourceHint: '重启时自动检测和更新站点资源包',
},
site: {
siteSync: '站点同步',
@@ -1614,7 +1621,7 @@ export default {
title: '插件市场设置',
repoUrl: '插件仓库地址',
repoPlaceholder: '格式https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
repoHint: '多个地址使用逗号分隔仅支持Github仓库',
repoHint: '多个地址使用换行分隔仅支持Github仓库',
close: '关闭',
save: '保存',
saveSuccess: '插件仓库保存成功',
@@ -1680,8 +1687,8 @@ export default {
previous: '上一步',
confirm: '确认',
manualTitle: '手动整理',
multipleItemsTitle: '整理 - 共 {count} 项',
singleItemTitle: '整理 - {path}',
multipleItemsTitle: '共 {count} 项',
singleItemTitle: '{path}',
targetStorage: '目的存储',
targetStorageHint: '整理目的存储',
transferType: '整理方式',
@@ -1730,7 +1737,7 @@ export default {
},
subscribeEdit: {
titleDefault: '默认订阅规则',
titleEditFormat: '编辑订阅 - {name} {season}',
titleEdit: '编辑订阅',
seasonFormat: '第 {number} 季',
tabs: {
basic: '基础',
@@ -1843,6 +1850,7 @@ export default {
peersColumn: '下载',
viewDetails: '查看详情',
downloadTorrent: '下载种子文件',
pageText: '{0}-{1} 共 {2} 条',
},
forkSubscribe: {
title: '复制订阅',
@@ -1974,6 +1982,52 @@ export default {
updateHistoryTitle: '{name} 更新说明',
updateToLatest: '更新到最新版本',
updatingTo: '更新 {name} 到 {version} 版本...',
folderNameEmpty: '文件夹名称不能为空',
folderExists: '文件夹已存在',
folderCreateSuccess: '文件夹创建成功',
folderRenameSuccess: '文件夹重命名成功',
folderRenameFailed: '重命名文件夹失败',
folderDeleteSuccess: '文件夹删除成功',
folderDeleteFailed: '删除文件夹失败',
removeFromFolderSuccess: '插件已移出文件夹',
operationFailed: '操作失败',
saveFolderConfigFailed: '保存文件夹配置失败',
newFolder: '新建文件夹',
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: '个人信息',
@@ -2355,4 +2409,22 @@ export default {
required: '此项为必填项',
number: '请输入数字',
},
folder: {
settingAppearance: '设置外观',
rename: '重命名',
deleteFolder: '删除文件夹',
folderNameCannotBeEmpty: '文件夹名称不能为空',
confirmDeleteFolder: '确定要删除文件夹 "{folderName}" 吗?文件夹中的插件将移回主列表。',
folderSettingsSaved: '文件夹设置已保存',
renameFolder: '重命名文件夹',
folderName: '文件夹名称',
folderAppearanceSettings: '文件夹外观设置',
showFolderIcon: '显示文件夹图标',
icon: '图标',
iconColor: '图标颜色',
backgroundGradient: '背景渐变',
customBackgroundImageURL: '自定义背景图片URL可选',
customBackgroundImageHint: '支持网络图片URL留空则使用渐变背景',
pluginCount: '{count} 个插件',
},
}

View File

@@ -39,6 +39,7 @@ export default {
unsubscribe: '取消訂閱',
media: '媒體',
unknown: '未知',
notice: '注意',
},
mediaType: {
movie: '電影',
@@ -851,8 +852,8 @@ export default {
browserSimulation: '使用瀏覽器模擬真實訪問該站點',
},
actions: {
add: '新增',
edit: '編輯',
add: '新增站點',
edit: '編輯站點',
},
messages: {
addSuccess: '新增站點成功',
@@ -1087,6 +1088,12 @@ export default {
securityImageDomainsHint: '允許緩存的圖片域名白名單,用於控制可信任的圖片來源',
noSecurityImageDomains: '暫無安全域名',
securityImageDomainAdd: '添加域名image.tmdb.org',
proxyHost: '代理服務器',
proxyHostHint: '設置代理服務器地址支持http(s)、socks5、socks5h 等協議',
moviePilotAutoUpdate: '自動更新MoviePilot',
moviePilotAutoUpdateHint: '重啟時自動更新MoviePilot到最新發行版本',
autoUpdateResource: '自動更新站點資源',
autoUpdateResourceHint: '重啟時自動檢測和更新站點資源包',
},
site: {
siteSync: '站點同步',
@@ -1615,7 +1622,7 @@ export default {
title: '插件市場設置',
repoUrl: '插件倉庫地址',
repoPlaceholder: '格式https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
repoHint: '多個地址使用逗號分隔僅支援Github倉庫',
repoHint: '多個地址使用换行分隔僅支援Github倉庫',
close: '關閉',
save: '儲存',
saveSuccess: '插件倉庫儲存成功',
@@ -1681,8 +1688,8 @@ export default {
previous: '上一步',
confirm: '確認',
manualTitle: '手動整理',
multipleItemsTitle: '整理 - 共 {count} 項',
singleItemTitle: '整理 - {path}',
multipleItemsTitle: '共 {count} 項',
singleItemTitle: '{path}',
targetStorage: '目的存儲',
targetStorageHint: '整理目的存儲',
transferType: '整理方式',
@@ -1731,7 +1738,7 @@ export default {
},
subscribeEdit: {
titleDefault: '默認訂閱規則',
titleEditFormat: '編輯訂閱 - {name} {season}',
titleEdit: '編輯訂閱',
seasonFormat: '第 {number} 季',
tabs: {
basic: '基礎',
@@ -1976,6 +1983,52 @@ export default {
updateHistoryTitle: '{name} 更新說明',
updateToLatest: '更新到最新版本',
updatingTo: '正在更新 {name} 至 v{version} ...',
folderNameEmpty: '文件夾名稱不能為空',
folderExists: '文件夾已存在',
folderCreateSuccess: '文件夾創建成功',
folderRenameSuccess: '文件夾重命名成功',
folderRenameFailed: '重命名文件夾失敗',
folderDeleteSuccess: '文件夾刪除成功',
folderDeleteFailed: '刪除文件夾失敗',
removeFromFolderSuccess: '插件已移出文件夾',
operationFailed: '操作失敗',
saveFolderConfigFailed: '保存文件夾配置失敗',
newFolder: '新建文件夾',
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: '個人信息',
@@ -2357,4 +2410,22 @@ export default {
required: '此項為必填項',
number: '請輸入數字',
},
folder: {
settingAppearance: '設定外觀',
rename: '重新命名',
deleteFolder: '刪除資料夾',
folderNameCannotBeEmpty: '資料夾名稱不能為空',
confirmDeleteFolder: '確定要刪除資料夾 "{folderName}" 嗎?資料夾中的插件將移回主列表。',
folderSettingsSaved: '資料夾設定已儲存',
renameFolder: '重新命名資料夾',
folderName: '資料夾名稱',
folderAppearanceSettings: '資料夾外觀設定',
showFolderIcon: '顯示資料夾圖示',
icon: '圖示',
iconColor: '圖示顏色',
backgroundGradient: '背景漸變',
customBackgroundImageURL: '自定義背景圖片URL可選',
customBackgroundImageHint: '支援網路圖片URL留空則使用漸變背景',
pluginCount: '{count} 個插件',
},
}

View File

@@ -24,7 +24,7 @@ import { fetchGlobalSettings } from './utils/globalSetting'
// 5. 其他插件和功能模块
import ToastPlugin from 'vue-toast-notification'
import VuetifyUseDialog from 'vuetify-use-dialog'
import ConfirmDialog from '@/composables/useConfirm'
import VueApexCharts from 'vue3-apexcharts'
// 6. 注册自定义组件
@@ -102,26 +102,7 @@ initializeApp().then(() => {
.use(ToastPlugin, {
position: 'bottom-right',
})
.use(VuetifyUseDialog, {
confirmDialog: {
dialogProps: {
maxWidth: '30rem',
},
confirmationButtonProps: {
variant: 'elevated',
color: 'primary',
class: 'me-3 px-5',
'prepend-icon': 'mdi-check',
},
cancellationButtonProps: {
variant: 'outlined',
color: 'secondary',
class: 'me-3',
},
confirmationText: i18n.global.t('common.confirm'),
cancellationText: i18n.global.t('common.cancel'),
},
})
.use(ConfirmDialog)
.use(i18n)
.mount('#app')
})

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

@@ -8,6 +8,7 @@ import DashboardElement from '@/components/misc/DashboardElement.vue'
import { useDisplay } from 'vuetify'
import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n'
import { VCardActions } from 'vuetify/components'
// 国际化
const { t } = useI18n()
@@ -353,7 +354,7 @@ onDeactivated(() => {
/>
<!-- 弹窗根据配置生成选项 -->
<VDialog v-if="dialog" v-model="dialog" max-width="35rem" max-height="85vh" scrollable>
<VDialog v-if="dialog" v-model="dialog" max-width="35rem" :fullscreen="!display.mdAndUp.value" scrollable>
<VCard>
<VCardItem>
<VCardTitle>
@@ -396,8 +397,7 @@ onDeactivated(() => {
<VSwitch v-model="isElevated" :label="t('dashboard.adaptiveHeight')" />
</p>
</VCardText>
<VDivider />
<VCardText class="pt-5 text-end">
<VCardActions class="pt-3">
<VSpacer />
<VBtn @click="saveDashboardConfig">
<template #prepend>
@@ -405,7 +405,7 @@ onDeactivated(() => {
</template>
{{ t('common.save') }}
</VBtn>
</VCardText>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -8,6 +8,9 @@ import ExtraSourceView from '@/views/discover/ExtraSourceView.vue'
import { DiscoverSource } from '@/api/types'
import api from '@/api'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -179,7 +182,13 @@ onActivated(async () => {
</VWindowItem>
</VWindow>
<!-- 弹窗根据配置生成选项 -->
<VDialog v-if="orderConfigDialog" v-model="orderConfigDialog" max-width="35rem" scrollable>
<VDialog
v-if="orderConfigDialog"
v-model="orderConfigDialog"
max-width="35rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VCardTitle>
@@ -199,16 +208,15 @@ onActivated(async () => {
:component-data="{ 'class': 'settings-grid' }"
>
<template #item="{ element }">
<div class="setting-item enabled">
<VCard variant="text" class="setting-item enabled">
<div class="setting-item-inner cursor-move text-center">
<span class="setting-label">{{ element.name }}</span>
</div>
</div>
</VCard>
</template>
</draggable>
</VCardText>
<VDivider />
<VCardText class="pt-5 text-end">
<VCardActions class="pt-3">
<VSpacer />
<VBtn @click="saveTabOrder">
<template #prepend>
@@ -216,7 +224,7 @@ onActivated(async () => {
</template>
{{ t('common.save') }}
</VBtn>
</VCardText>
</VCardActions>
</VCard>
</VDialog>
<!-- 快速滚动到顶部按钮 -->
@@ -261,6 +269,7 @@ onActivated(async () => {
&::before {
position: absolute;
background-color: transparent;
background-color: rgb(var(--v-theme-primary));
block-size: 100%;
content: '';
inline-size: 4px;

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

@@ -3,6 +3,9 @@ import api from '@/api'
import { RecommendSource } from '@/api/types'
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -235,7 +238,7 @@ onActivated(async () => {
</div>
<!-- 设置面板 -->
<VDialog v-model="dialog" width="35rem" class="settings-dialog" scrollable>
<VDialog v-model="dialog" width="35rem" class="settings-dialog" scrollable :fullscreen="!display.mdAndUp.value">
<VCard class="settings-card">
<VCardItem class="settings-card-header">
<VCardTitle>
@@ -248,7 +251,7 @@ onActivated(async () => {
<VCardText>
<p class="settings-hint">{{ t('recommend.selectContentToDisplay') }}</p>
<div class="settings-grid">
<div
<VCard
v-for="item in viewList"
:key="item.title"
class="setting-item"
@@ -268,11 +271,10 @@ onActivated(async () => {
</div>
<span class="setting-label">{{ item.title }}</span>
</div>
</div>
</VCard>
</div>
</VCardText>
<VDivider />
<VCardActions class="pt-5">
<VCardActions class="pt-3">
<VBtn variant="text" @click="Object.keys(enableConfig).forEach(key => (enableConfig[key] = true))">
{{ t('recommend.selectAll') }}
</VBtn>
@@ -280,7 +282,7 @@ onActivated(async () => {
{{ t('recommend.selectNone') }}
</VBtn>
<VSpacer />
<VBtn @click="saveConfig" variant="elevated" color="primary" class="px-5">
<VBtn @click="saveConfig" color="primary" class="px-5">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>

View File

@@ -39,7 +39,7 @@ export function getBrowserLocale(): SupportedLocale | null {
return navigatorLocale.includes(locale.split('-')[0])
})
return (locale as SupportedLocale) || zh-CN
return (locale as SupportedLocale) || 'zh-CN'
}
/**

View File

@@ -58,6 +58,10 @@ html.v-overlay-scroll-blocked {
margin-block-start: env(safe-area-inset-top);
}
.v-dialog > .v-overlay__content > .v-card > .v-card-item {
padding: 16px;
}
/* router view transition fade-slide */
.fade-slide-leave-active,
.fade-slide-enter-active {
@@ -221,7 +225,7 @@ html.v-overlay-scroll-blocked {
}
.grid-workflow-card {
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {

View File

@@ -69,7 +69,7 @@ onActivated(() => {
</template>
<VCardTitle>{{ t('dashboard.library') }}</VCardTitle>
</VCardItem>
<div class="grid gap-4 grid-backdrop-card mx-3" tabindex="0">
<div class="grid gap-4 grid-backdrop-card mx-3 mb-3" tabindex="0">
<LibraryCard v-for="item in libraryList" :key="item.id" :media="item" height="10rem" />
</div>
</VCard>

View File

@@ -70,7 +70,7 @@ onActivated(() => {
<VCardTitle>{{ t('dashboard.playing') }}</VCardTitle>
</VCardItem>
<div class="grid gap-4 grid-backdrop-card mx-3" tabindex="0">
<div class="grid gap-4 grid-backdrop-card mx-3 mb-3" tabindex="0">
<BackdropCard v-for="item in playingList" :key="item.id" :media="item" height="10rem" />
</div>
</VCard>

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">

File diff suppressed because it is too large Load Diff

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

@@ -7,7 +7,10 @@ import NotificationChannelCard from '@/components/cards/NotificationChannelCard.
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n'
import { notificationSwitchDict } from '@/api/constants'
import { useTheme } from 'vuetify'
import { useTheme, useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -290,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">
@@ -395,11 +400,12 @@ onMounted(() => {
</tr>
</tbody>
</VTable>
<VDivider />
<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>
@@ -416,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>
@@ -441,13 +459,18 @@ onMounted(() => {
:indeterminate="true"
/>
<!-- 模板编辑器对话框 -->
<VDialog v-model="editorVisible" v-if="editorVisible" max-width="50rem">
<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">
@@ -455,11 +478,11 @@ onMounted(() => {
v-model:value="editorContent"
lang="json"
:theme="editorTheme"
class="w-full min-h-[30rem] rounded"
class="w-full h-full min-h-[30rem] rounded"
/>
</VCardText>
<VCardActions class="mx-auto pt-3">
<VBtn variant="elevated" color="primary" @click="saveTemplate" prepend-icon="mdi-content-save" class="px-5">
<VCardActions class="pt-3">
<VBtn color="primary" @click="saveTemplate" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>

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

@@ -11,6 +11,9 @@ import { copyToClipboard } from '@/@core/utils/navigator'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n'
import { downloaderOptions, mediaServerOptions } from '@/api/constants'
import { useDisplay } from 'vuetify'
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -37,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,
@@ -382,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()
@@ -423,6 +438,7 @@ onDeactivated(() => {
:hint="t('setting.system.appDomainHint')"
placeholder="http://localhost:3000"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
@@ -435,6 +451,7 @@ onDeactivated(() => {
:hint="t('setting.system.wallpaperHint')"
persistent-hint
:items="wallpaperItems"
prepend-inner-icon="mdi-image"
/>
</VCol>
@@ -446,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>
@@ -460,6 +478,7 @@ onDeactivated(() => {
{ title: 'TheMovieDb', value: 'themoviedb' },
{ title: '豆瓣', value: 'douban' },
]"
prepend-inner-icon="mdi-database"
/>
</VCol>
<VCol cols="12" md="6">
@@ -476,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">
@@ -485,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'),
@@ -502,6 +523,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.githubTokenFormat')"
:hint="t('setting.system.githubTokenHint')"
persistent-hint
prepend-inner-icon="mdi-github"
>
</VTextField>
</VCol>
@@ -512,6 +534,7 @@ onDeactivated(() => {
placeholder="https://movie-pilot.org"
:hint="t('setting.system.ocrHostHint')"
persistent-hint
prepend-inner-icon="mdi-text-recognition"
/>
</VCol>
</VRow>
@@ -520,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"
@@ -565,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>
@@ -613,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>
@@ -633,8 +662,15 @@ onDeactivated(() => {
</VCard>
</VCol>
</VRow>
<!-- 高级系统设置 -->
<VDialog v-if="advancedDialog" v-model="advancedDialog" scrollable max-width="60rem">
<VDialog
v-if="advancedDialog"
v-model="advancedDialog"
scrollable
max-width="60rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem>
<VDialogCloseBtn @click="advancedDialog = false" />
@@ -711,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>
@@ -726,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">
@@ -737,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">
@@ -747,6 +801,7 @@ onDeactivated(() => {
:hint="t('setting.system.tmdbLocaleHint')"
persistent-hint
:items="tmdbLanguageItems"
prepend-inner-icon="mdi-translate"
/>
</VCol>
<VCol cols="12" md="6">
@@ -762,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>
@@ -796,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"
@@ -805,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')"
@@ -816,6 +883,7 @@ onDeactivated(() => {
persistent-hint
:items="pipMirrorsItems"
clearable
prepend-inner-icon="mdi-package"
/>
</VCol>
</VRow>
@@ -835,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">
@@ -844,6 +913,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.dohDomainsPlaceholder')"
:hint="t('setting.system.dohDomainsHint')"
persistent-hint
prepend-inner-icon="mdi-domain"
/>
</VCol>
</VRow>
@@ -875,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">
@@ -908,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">
@@ -920,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">
@@ -931,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">
@@ -939,6 +1013,7 @@ onDeactivated(() => {
:label="t('setting.system.logFileFormat')"
:hint="t('setting.system.logFileFormatHint')"
persistent-hint
prepend-inner-icon="mdi-format-text"
/>
</VCol>
</VRow>
@@ -979,13 +1054,7 @@ onDeactivated(() => {
<VCardActions class="pt-3">
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn
color="primary"
variant="elevated"
prepend-icon="mdi-content-save"
@click="saveAdvancedSettings"
class="px-5"
>
<VBtn color="primary" prepend-icon="mdi-content-save" @click="saveAdvancedSettings" class="px-5">
{{ t('common.save') }}
</VBtn>
</div>

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

@@ -3,6 +3,10 @@ import { cloneDeepWith } from 'lodash-es'
import type { Context } from '@/api/types'
import TorrentCard from '@/components/cards/TorrentCard.vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -606,7 +610,13 @@ const handleSortIconClick = () => {
</VCard>
<!-- 全部筛选弹窗 -->
<VDialog v-model="allFilterMenuOpen" max-width="50rem" max-height="90%" location="center" scrollable>
<VDialog
v-model="allFilterMenuOpen"
max-width="50rem"
location="center"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VDialogCloseBtn @click="allFilterMenuOpen = false" />
<VCardTitle class="py-3 d-flex align-center">
@@ -676,7 +686,7 @@ const handleSortIconClick = () => {
</VDialog>
<!-- 筛选弹窗 -->
<VDialog v-model="filterMenuOpen" max-width="25rem" max-height="80%" location="center">
<VDialog v-model="filterMenuOpen" max-width="25rem" location="center" max-height="85vh">
<VCard>
<VCardTitle class="py-3 d-flex align-center">
<VIcon :icon="getFilterIcon(currentFilter)" class="me-2"></VIcon>
@@ -713,7 +723,7 @@ const handleSortIconClick = () => {
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" color="primary" @click="filterMenuOpen = false">
<VBtn color="primary" prepend-icon="mdi-check" class="px-5" @click="filterMenuOpen = false">
{{ t('torrent.confirm') }}
</VBtn>
</VCardActions>

View File

@@ -2,6 +2,10 @@
import type { Context } from '@/api/types'
import TorrentItem from '@/components/cards/TorrentItem.vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 国际化
const { t } = useI18n()
@@ -582,7 +586,13 @@ onMounted(() => {
</VCard>
<!-- 全部筛选弹窗 -->
<VDialog v-model="allFilterMenuOpen" max-width="50rem" max-height="90%" location="center" scrollable>
<VDialog
v-model="allFilterMenuOpen"
max-width="50rem"
location="center"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VDialogCloseBtn @click="allFilterMenuOpen = false" />
<VCardTitle class="py-3 d-flex align-center">
@@ -652,7 +662,7 @@ onMounted(() => {
</VDialog>
<!-- 筛选弹窗 -->
<VDialog v-model="filterMenuOpen" max-width="25rem" max-height="80%" location="center">
<VDialog v-model="filterMenuOpen" max-width="25rem" max-height="85vh" location="center">
<VCard>
<VCardTitle class="py-3 d-flex align-center">
<VIcon :icon="getFilterIcon(currentFilter)" class="me-2"></VIcon>
@@ -689,7 +699,7 @@ onMounted(() => {
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" color="primary" @click="filterMenuOpen = false">
<VBtn color="primary" prepend-icon="mdi-check" class="px-5" @click="filterMenuOpen = false">
{{ t('torrent.confirm') }}
</VBtn>
</VCardActions>

View File

@@ -114,7 +114,6 @@ useDynamicButton({
v-model="addUserDialog"
oper="add"
max-width="45rem"
persistent
@save="onUserAdd"
@close="addUserDialog = false"
/>

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>

View File

@@ -7704,13 +7704,6 @@ vuedraggable@^4.1.0:
dependencies:
sortablejs "1.14.0"
vuetify-use-dialog@^0.6.11:
version "0.6.11"
resolved "https://registry.yarnpkg.com/vuetify-use-dialog/-/vuetify-use-dialog-0.6.11.tgz#8800cc56b234dae1dfa44a7f06a6bb1a33ad4b39"
integrity sha512-iPAu6MsN8suuNAS1M6JN2CaOXRgr7LZ2u+UNtAw0Fi3AjianzVIrnRNhQcAZjmE8Hu6ZwAbgte1p47qU6OazLw==
dependencies:
defu "^6.1.4"
vuetify@3.7.3:
version "3.7.3"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.7.3.tgz#0e89f7f0298d452510bcbc01b0e9b53a5ce6e883"