Compare commits

...

60 Commits

Author SHA1 Message Date
jxxghp
157c37c862 add service worker 2024-06-05 18:12:07 +08:00
jxxghp
da910ac670 Merge pull request #151 from hotlcc/develop-20240604-2 2024-06-04 17:53:02 +08:00
Allen
3831363815 删除和整理场景路由参数未改变,reloadPage不会生效,需要fetchData刷新数据 2024-06-04 17:41:09 +08:00
jxxghp
94a6ea13bd rollback 2024-06-04 16:18:06 +08:00
jxxghp
06c1ad0f69 更新 main.ts 2024-06-04 16:14:25 +08:00
jxxghp
d6873781e8 更新 SearchBarView.vue 2024-06-04 15:46:36 +08:00
jxxghp
ab6c9647a7 Merge pull request #149 from hotlcc/develop-20240604-1 2024-06-04 15:42:36 +08:00
Allen
59b0350993 针对异形屏做了优化 2024-06-04 15:28:17 +08:00
jxxghp
df0be4c070 更新 package.json 2024-06-04 14:02:59 +08:00
jxxghp
87f3ef4353 Merge pull request #148 from hotlcc/develop-20240604-1 2024-06-04 14:00:49 +08:00
Allen
2611bbaea4 弹窗 VDialog 在低版本 iOS Safari 浏览器下宽度异常问题处理 2024-06-04 13:54:48 +08:00
jxxghp
7c0d8cf792 Merge pull request #147 from hotlcc/develop-20240604-1 2024-06-04 12:54:09 +08:00
Allen
2d17baccd2 低版本safari主菜单样式兼容性处理 2024-06-04 12:33:18 +08:00
jxxghp
fe31723726 fix #145 2024-06-04 11:41:38 +08:00
jxxghp
bb10b22421 fix bug 2024-06-04 08:01:10 +08:00
jxxghp
6445f3a634 Merge pull request #144 from falling/main 2024-06-03 21:11:33 +08:00
falling
d1f28d9c94 资源搜索里的季集下拉列表,从字符串排序改成按季集排序 2024-06-03 21:01:12 +08:00
jxxghp
1e5366123c feat:近期搜索记忆 2024-06-03 16:35:32 +08:00
jxxghp
7feff7c90b fix 2024-06-03 11:45:15 +08:00
jxxghp
429b3bc045 Merge pull request #142 from hotlcc/develop-20240603
Develop 20240603
2024-06-03 11:36:01 +08:00
Allen
e76f1b89da fix number 2024-06-03 11:33:53 +08:00
Allen
f25e8595c3 fix number 2024-06-03 11:17:54 +08:00
jxxghp
6977ce55a3 Merge pull request #141 from hotlcc/develop-20240603
Develop 20240603
2024-06-03 11:09:51 +08:00
Allen
222e0e5ff2 fix encodeURIComponent 2024-06-03 11:04:17 +08:00
Allen
6996d9bbe2 历史记录页面搜索关键字、页码、页大小参数路优化,方便外部定位,同时为了解决支持kbar后路由参数和搜索框内容不一致的问题 2024-06-03 11:00:00 +08:00
jxxghp
f70e08adac Merge pull request #140 from hotlcc/develop-20240603
Develop 20240603
2024-06-03 10:47:04 +08:00
jxxghp
223ecc0e6b fix dialog persistent-hint 2024-06-03 10:43:28 +08:00
Allen
43f36f556c kbar支持历史记录 2024-06-03 10:16:01 +08:00
jxxghp
4579e00283 fix persistent-hint 2024-06-03 10:14:03 +08:00
Allen
b5e9b14048 站点卡片代理和仿真图标顺序与配置界面保存一致 2024-06-03 09:21:03 +08:00
Allen
2288e72c5f 站点卡片有代理等图标时高度保持一致 2024-06-03 09:19:52 +08:00
jxxghp
4882cc0417 release v1.9.3 2024-06-03 08:21:15 +08:00
jxxghp
499d3d0424 fix ui 2024-06-03 08:09:03 +08:00
jxxghp
d6b17debb4 fix loading banner 2024-06-02 21:27:24 +08:00
jxxghp
8f970e0008 feat:支持直接搜索站点资源 2024-06-02 21:10:02 +08:00
jxxghp
18d778a1cc feat:聚合搜索支持订阅 2024-06-02 19:50:28 +08:00
jxxghp
d667c4e45d v1.9.3 2024-06-02 18:50:55 +08:00
jxxghp
b7f8ffd56f fix 聚合搜索 2024-06-02 18:45:50 +08:00
jxxghp
c20f9d527f fix icon 2024-06-02 15:41:09 +08:00
jxxghp
b859d00cb9 fix 聚合搜索 2024-06-02 14:58:58 +08:00
jxxghp
a2d28ad360 feat:聚合搜索(working...) 2024-06-02 11:13:03 +08:00
jxxghp
c6702fbc18 更新 package.json 2024-06-01 22:31:44 +08:00
jxxghp
5018f96786 fix VFab 2024-06-01 22:07:19 +08:00
jxxghp
f29f408b67 feat:目录选择组件 2024-05-31 20:35:57 +08:00
jxxghp
a475a3b851 fix DirectoryTreeInput 2024-05-31 18:31:45 +08:00
jxxghp
9335f79c30 add DirectoryTreeInput 2024-05-31 15:06:58 +08:00
jxxghp
9dab691649 Merge pull request #139 from hotlcc/develop-20240531
vuetify升级至3.6.8后,设定中tab的选中样式改变,修复为原版选中样式
2024-05-31 14:10:36 +08:00
Allen
16abc65f49 vuetify升级至3.6.8后,设定中tab的选中样式改变,修复为原版选中样式 2024-05-31 13:00:35 +08:00
jxxghp
23ac80886d upgrade vuetify to 3.6.8 2024-05-31 11:46:27 +08:00
jxxghp
b242e757e0 add kbar 2024-05-31 11:26:19 +08:00
jxxghp
a69965a605 Merge pull request #138 from hotlcc/develop-20240531 2024-05-31 11:25:14 +08:00
Allen
3321427eb4 修正tab路由参数为query,解决原先采用params路由参数时导致主菜单选中状态不同步的问题 2024-05-31 11:18:44 +08:00
jxxghp
3ffe354770 v1.9.2-3 2024-05-31 08:04:48 +08:00
jxxghp
52e0d3a4bc fix tab route 2024-05-31 08:04:27 +08:00
jxxghp
e865a5ca62 Merge pull request #136 from hotlcc/develop-20240530 2024-05-30 15:24:06 +08:00
Allen
528a4ddb03 完善设定tab精确路由 2024-05-30 15:16:49 +08:00
jxxghp
36f3b649c6 Merge pull request #135 from hotlcc/develop-20240530 2024-05-30 11:59:54 +08:00
Allen
ce91c0cc30 await接口请求后才重新获取插件仪表板,解决仪表板调整配置保存时出现重复插件请求的问题 2024-05-30 11:36:13 +08:00
jxxghp
e31e9e3520 更新 dashboard.vue 2024-05-29 15:30:04 +08:00
jxxghp
df313ebe7f fix 2024-05-29 15:26:59 +08:00
63 changed files with 2528 additions and 1358 deletions

View File

@@ -1 +1,2 @@
VITE_API_BASE_URL=http://localhost:3001/api/v1/
VITE_PUBLIC_VAPID_KEY=BH3w49sZA6jXUnE-yt4jO6VKh73lsdsvwoJ6Hx7fmPIDKoqGiUl2GEoZzy-iJfn4SfQQcx7yQdHf9RknwrL_lSM

View File

@@ -1 +1,2 @@
VITE_API_BASE_URL=api/v1/
VITE_PUBLIC_VAPID_KEY=BH3w49sZA6jXUnE-yt4jO6VKh73lsdsvwoJ6Hx7fmPIDKoqGiUl2GEoZzy-iJfn4SfQQcx7yQdHf9RknwrL_lSM

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@ node_modules
.DS_Store
dist
dist-ssr
dev-dist
*.local
/cypress/videos/

View File

@@ -15,7 +15,6 @@
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "1.9.2-2",
"version": "1.9.4-beta",
"private": true,
"bin": "dist/service.js",
"scripts": {
@@ -36,13 +36,12 @@
"express": "^4.18.2",
"express-http-proxy": "^2.0.0",
"lodash": "^4.17.21",
"mousetrap": "^1.6.5",
"nprogress": "^0.2.0",
"pull-refresh-vue3": "^0.3.1",
"qrcode.vue": "^3.4.1",
"sass": "^1.59.3",
"tailwindcss": "^3.3.2",
"unplugin-vue-define-options": "^1.3.5",
"vite-plugin-pwa": "^0.19.8",
"vue": "^3.3.2",
"vue-router": "^4.2.0",
"vue-toast-notification": "^3",
@@ -50,7 +49,7 @@
"vue3-apexcharts": "^1.4.1",
"vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0",
"vuetify": "3.5.14",
"vuetify": "3.6.8",
"vuetify-use-dialog": "^0.6.11",
"vuex": "^4.1.0",
"vuex-persistedstate": "^4.1.0",
@@ -92,6 +91,7 @@
"unplugin-vue-components": "^0.26.0",
"vite": "^5.2.8",
"vite-plugin-pages": "^0.32.1",
"vite-plugin-pwa": "^0.20.0",
"vite-plugin-vue-layouts": "^0.11.0",
"vite-plugin-vuetify": "2.0.3",
"vue-shepherd": "^3.0.0",
@@ -101,4 +101,4 @@
"resolutions": {
"postcss": "8"
}
}
}

View File

@@ -1,80 +0,0 @@
{
"name": "MoviePilot",
"short_name": "MoviePilot",
"start_url": "./",
"display": "standalone",
"icons": [
{
"src": "./android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "./android-chrome-192x192_maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "./android-chrome-512x512_maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#28243D",
"background_color": "#28243D",
"shortcuts": [
{
"name": "推荐",
"url": "./ranking",
"icons": [
{
"src": "./sparkles-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "电影订阅",
"url": "./subscribe-movie",
"icons": [
{
"src": "./clock-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "电视剧订阅",
"url": "./subscribe-tv",
"icons": [
{
"src": "./clock-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "设置",
"url": "./setting",
"icons": [
{
"src": "./cog-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
]
}

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -14,7 +14,10 @@ function onClick() {
</script>
<template>
<IconBtn :class="props.innerClass ? props.innerClass : 'absolute right-3 top-3'" @click.stop="onClick">
<IconBtn
:class="props.innerClass ? props.innerClass : 'absolute right-3 top-3'"
@click.stop="onClick"
>
<VIcon icon="mdi-close" />
</IconBtn>
</template>

View File

@@ -1,28 +1,15 @@
<script lang="ts" setup>
// 定义输入参数
const props = defineProps({
progress: Number,
text: String
})
// 定义输入参数
const props = defineProps({
progress: Number,
text: String,
})
</script>
<template>
<div
class="w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!props.text"
size="48"
indeterminate
color="primary"
/>
<VProgressCircular
v-if="props.progress"
class="mb-3"
color="primary"
:model-value="props.progress"
size="64"
/>
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center">
<VProgressCircular v-if="!props.text || !props.progress" class="mb-3" size="48" indeterminate color="primary" />
<VProgressCircular v-if="props.progress" class="mb-3" color="primary" :model-value="props.progress" size="64" />
<span>{{ props.text }}</span>
</div>
</template>

View File

@@ -5,14 +5,14 @@
/**
* 修复低版本Safari等浏览器数组不支持at函数的问题
*/
export function fixArrayAt() {
if (!Array.prototype.at) {
Array.prototype.at = function(index: number) {
if (index >= 0) {
return this[index]
} else {
return this[this.length + index]
}
}
;(function fixArrayAt() {
if (!Array.prototype.at) {
Array.prototype.at = function (index: number) {
if (index >= 0) {
return this[index]
} else {
return this[this.length + index]
}
}
}
}
})()

View File

@@ -28,3 +28,17 @@ export async function copyToClipboard(content: string) {
document.body.removeChild(input)
}
}
// VAPID公钥转Uint8Array
export function urlBase64ToUint8Array(base64String: string) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = window.atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}

View File

@@ -34,6 +34,10 @@ defineProps<{
display: flex;
align-items: center;
cursor: pointer;
padding-left: 1.375rem;
padding-right: 1rem;
margin-right: 1.125em;
border-radius: 0 3.125rem 3.125rem 0 !important;
}
}
</style>

View File

@@ -18,3 +18,12 @@ defineProps<{
</div>
</li>
</template>
<style lang="scss">
.layout-vertical-nav {
.nav-section-title {
padding-left: 1.375rem;
padding-right: 1rem;
}
}
</style>

View File

@@ -120,6 +120,12 @@ export interface NavLink extends NavLinkProps, Partial<AclProperties> {
disable?: boolean
}
export interface NavMenu extends NavLink {
header: string
admin: boolean
description?: string
}
// 👉 Vertical nav group
export interface NavGroup extends Partial<AclProperties> {
title: string

View File

@@ -8,32 +8,33 @@ const api = axios.create({
})
// 添加请求拦截器
api.interceptors.request.use((config) => {
api.interceptors.request.use(config => {
// 在请求头中添加token
const token = store.state.auth.token
if (token)
config.headers.Authorization = `Bearer ${token}`
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// 添加响应拦截器
api.interceptors.response.use((response) => {
return response.data
}, (error) => {
if (!error.response) {
// 请求超时
return Promise.reject(error)
}
else if (error.response.status === 403) {
// 清除登录状态信息
store.dispatch('auth/clearToken')
api.interceptors.response.use(
response => {
return response.data
},
error => {
if (!error.response) {
// 请求超时
return Promise.reject(new Error(error))
} else if (error.response.status === 403) {
// 清除登录状态信息
store.dispatch('auth/clearToken')
// token验证失败跳转到登录页面
router.push('/login')
}
// token验证失败跳转到登录页面
router.push('/login')
}
return Promise.reject(error)
})
return Promise.reject(new Error(error))
},
)
export default api

View File

@@ -58,11 +58,13 @@ export interface Subscribe {
// 当前优先级
current_priority: number
// 保存目录
save_path: string
save_path?: string
// 时间
date: string
// 编辑框设置项
show_edit_dialog: boolean
// 编辑框打开状态
page_open?: boolean
}
// 历史记录
@@ -445,6 +447,8 @@ export interface Plugin {
history?: { [key: string]: string }
// 添加时间
add_time?: number
// 页面打开状态
page_open?: boolean
}
// 渲染结构
@@ -736,7 +740,7 @@ export interface EndPoints {
// 文件浏览项目
export interface FileItem {
// 类型
// 类型 dir/file
type: string
// 文件名
name: string

View File

@@ -17,6 +17,9 @@ const props = defineProps({
height: String,
})
//
const path = ref<string>('')
//
const typeItems = [
{ title: '全部', value: '' },
@@ -25,13 +28,19 @@ const typeItems = [
]
//
const emit = defineEmits(['close', 'changed'])
const emit = defineEmits(['close', 'changed', 'update:modelValue'])
//
function onClose() {
emit('close')
}
//
function updatePath(value: string) {
path.value = value
emit('update:modelValue', value)
}
//
const getCategories = computed(() => {
const default_value = [{ title: '全部', value: '' }]
@@ -60,7 +69,11 @@ const getCategories = computed(() => {
<VForm>
<VRow>
<VCol>
<VTextField v-model="props.directory.path" variant="underlined" label="路径" />
<VPathField @update:modelValue="updatePath">
<template #activator="{ menuprops }">
<VTextField v-model="props.directory.path" v-bind="menuprops" variant="underlined" label="路径" />
</template>
</VPathField>
</VCol>
</VRow>
<VRow>

View File

@@ -365,11 +365,19 @@ const dropdownItems = ref([
// 监听插件状态变化
watch(
() => props.plugin?.has_update,
(newHasUpdate, oldHasUpdate) => {
(newHasUpdate, _) => {
const updateItemIndex = dropdownItems.value.findIndex(item => item.value === 3)
if (updateItemIndex !== -1) dropdownItems.value[updateItemIndex].show = newHasUpdate
},
)
// 监听插件窗口状态变化
watch(
() => props.plugin?.page_open,
(newOpenState, _) => {
if (newOpenState) openPluginDetail()
},
)
</script>
<template>
@@ -456,7 +464,7 @@ watch(
<VCardText class="min-h-40">
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
</VCardText>
<VFab icon="mdi-cog" location="bottom end" size="x-large" fixed app appear @click="showPluginConfig" />
<VFab icon="mdi-cog" location="bottom" size="x-large" fixed app appear @click="showPluginConfig" />
</VCard>
</VDialog>

View File

@@ -194,7 +194,7 @@ onMounted(() => {
<VImg :src="siteIcon" />
</VAvatar>
</template>
<VCardItem>
<VCardItem style="padding-block-end: 0;">
<VCardTitle class="font-bold">
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
</VCardTitle>
@@ -202,10 +202,10 @@ onMounted(() => {
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
</VCardSubtitle>
</VCardItem>
<VCardText class="py-2">
<VTooltip v-if="cardProps.site?.render === 1" text="浏览器仿真">
<VCardText class="py-2" style="block-size: 36px;">
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
<template #activator="{ props }">
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
</template>
</VTooltip>
<VTooltip v-if="cardProps.site?.proxy === 1" text="代理">
@@ -213,9 +213,9 @@ onMounted(() => {
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
</template>
</VTooltip>
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
<VTooltip v-if="cardProps.site?.render === 1" text="浏览器仿真">
<template #activator="{ props }">
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
</template>
</VTooltip>
<VTooltip v-if="cardProps.site?.filter" text="过滤">

View File

@@ -133,6 +133,14 @@ const dropdownItems = ref([
},
},
])
// 监听插件窗口状态变化
watch(
() => props.media?.page_open,
(newOpenState, _) => {
if (newOpenState) editSubscribeDialog()
},
)
</script>
<template>

View File

@@ -70,18 +70,24 @@ async function handleAddDownload(_site: any = undefined, _media: any = undefined
async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
startNProgress()
try {
const result: { [key: string]: any } = await api.post('download/', {
media_in: _media,
torrent_in: _torrent,
})
let result: { [key: string]: any }
if (result.success) {
if (_media) {
result = await api.post('download/', {
media_in: _media,
torrent_in: _torrent,
})
} else {
result = await api.post('download/add', _torrent)
}
if (result && result.success) {
// 添加下载成功
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 下载成功!`)
downloaded.value.push(_torrent?.enclosure || '')
} else {
// 添加下载失败
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 下载失败${result?.message}`)
}
} catch (error) {
console.error(error)
@@ -127,7 +133,7 @@ onMounted(() => {
</template>
<VCardItem class="py-1">
<VCardTitle class="break-words overflow-visible whitespace-break-spaces">
{{ media?.title }} {{ meta?.season_episode }}
{{ media?.title ?? meta?.name }} {{ meta?.season_episode }}
<span class="text-green-700 ms-2 text-sm">{{ torrent?.seeders }}</span>
<span class="text-orange-700 ms-2 text-sm">{{ torrent?.peers }}</span>
</VCardTitle>

View File

@@ -67,18 +67,24 @@ async function handleAddDownload(_site: any = undefined, _media: any = undefined
async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
startNProgress()
try {
const result: { [key: string]: any } = await api.post('download/', {
media_in: _media,
torrent_in: _torrent,
})
let result: { [key: string]: any }
if (result.success) {
if (_media) {
result = await api.post('download/', {
media_in: _media,
torrent_in: _torrent,
})
} else {
result = await api.post('download/add', _torrent)
}
if (result && result.success) {
// 添加下载成功
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 下载成功!`)
downloaded.value.push(_torrent?.enclosure || '')
} else {
// 添加下载失败
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 下载失败${result?.message}`)
}
} catch (error) {
console.error(error)

View File

@@ -214,7 +214,8 @@ onMounted(() => {
:items="targetDirectories"
label="目的路径"
placeholder="留空自动"
hint="留空将自动匹配目标路径"
hint="整理目的路径,留空将自动匹配"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -230,6 +231,8 @@ onMounted(() => {
{ title: 'Rclone复制', value: 'rclone_copy' },
{ title: 'Rclone移动', value: 'rclone_move' },
]"
hint="文件操作整理方式"
persistent-hint
/>
</VCol>
</VRow>
@@ -243,6 +246,8 @@ onMounted(() => {
{ title: '电影', value: '电影' },
{ title: '电视剧', value: '电视剧' },
]"
hint="文件的媒体类型"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -254,7 +259,8 @@ onMounted(() => {
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
hint="点击图标按名称搜索,留空自动重新识别"
hint="按名称查询媒体编号,留空自动识别"
persistent-hint
@click:append-inner="mediaSelectorDialog = true"
/>
<VTextField
@@ -265,7 +271,8 @@ onMounted(() => {
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
hint="点击图标按名称搜索,留空自动重新识别"
hint="按名称查询媒体编号,留空自动识别"
persistent-hint
@click:append-inner="mediaSelectorDialog = true"
/>
</VCol>
@@ -275,6 +282,8 @@ onMounted(() => {
v-model.number="transferForm.season"
label="季"
:items="seasonItems"
hint="指定季数"
persistent-hint
/>
</VCol>
</VRow>
@@ -284,7 +293,8 @@ onMounted(() => {
v-model="transferForm.episode_format"
label="集数定位"
placeholder="使用{ep}定位集数"
hint="使用{ep}定位文件名中的集数部分,其余相同部分直接填写,不同部分使用{a}进行忽略,例如:{a}葬送的芙莉莲_Sousou no Frieren 第{ep}话{b}"
hint="使用{ep}定位文件名中的集数部分以辅助识别"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -292,7 +302,8 @@ onMounted(() => {
v-model="transferForm.episode_detail"
label="指定集数"
placeholder="起始集,终止集如1或1,2"
hint="直接指定集数或范围,格式:起始集,终止集,如1或1,2"
hint="指定集数或范围如1或1,2"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -300,7 +311,8 @@ onMounted(() => {
v-model="transferForm.episode_part"
label="指定Part"
placeholder="如part1"
hint="指定集数的Part如part1"
hint="指定Part如part1"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -308,7 +320,8 @@ onMounted(() => {
v-model.number="transferForm.episode_offset"
label="集数偏移"
placeholder="如-10"
hint="集数进行偏移运算,如-10表示文件名中的集数减10为整理后集数"
hint="集数偏移运算,如-10或EP*2"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -317,13 +330,19 @@ onMounted(() => {
label="最小文件大小MB"
:rules="[numberValidator]"
placeholder="0"
hint="最小文件大小,小于此大小的文件将被忽略不进行整理"
hint="只整理大于最小文件大小的文件"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch v-model="transferForm.scrape" label="刮削元数据" hint="整理完成后自动刮削元数据" />
<VSwitch
v-model="transferForm.scrape"
label="刮削元数据"
hint="整理完成后自动刮削元数据"
persistent-hint
/>
</VCol>
</VRow>
</VForm>

View File

@@ -143,6 +143,7 @@ async function updateSiteInfo() {
label="站点地址"
:rules="[requiredValidator]"
hint="格式http://www.example.com/"
persistent-hint
/>
</VCol>
<VCol cols="6" md="3">
@@ -151,11 +152,18 @@ async function updateSiteInfo() {
label="优先级"
:items="priorityItems"
:rules="[requiredValidator]"
hint="站点资源下载优先级,优先级数字越小越优先下载"
hint="优先级越小越优先"
persistent-hint
/>
</VCol>
<VCol cols="6" md="3">
<VSelect v-model="siteForm.is_active" :items="statusItems" label="状态" />
<VSelect
v-model="siteForm.is_active"
:items="statusItems"
label="状态"
hint="站点启用/停用"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
@@ -163,34 +171,38 @@ async function updateSiteInfo() {
<VTextField
v-model="siteForm.rss"
label="RSS地址"
hint="订阅模式为站点RSS时,将会使用此地址获取站点种子资源,该地址一般会自动获取,也可手动补充"
hint="订阅模式为`站点RSS`时使用的订阅链接,如未自动获取手动补充"
persistent-hint
/>
</VCol>
<VCol cols="12" md="3">
<VTextField v-model="siteForm.timeout" label="超时时间(秒)" hint="站点请求超时时间,为空将使用默认值" />
<VTextField v-model="siteForm.timeout" label="超时时间(秒)" hint="站点请求超时时间" persistent-hint />
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"
label="站点Cookie"
hint="浏览器打开站点首页打开开发人员工具刷新页面后在网络选项中找到首页地址在请求头中获取Cookie信息"
/>
<VTextarea v-model="siteForm.cookie" label="站点Cookie" hint="站点请求头中的Cookie信息" persistent-hint />
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="siteForm.token"
label="请求头Authorization"
hint="在开发人员工具,网络请求头中获取Authorization,仅个别站点需要"
hint="站点请求头中Authorization信息,特殊站点需要"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="siteForm.apikey" label="令牌API Key" hint="站点的访问API Key仅个别站点需要" />
<VTextField
v-model="siteForm.apikey"
label="令牌API Key"
hint="站点的访问API Key特殊站点需要"
persistent-hint
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="siteForm.ua"
label="站点User-Agent"
hint="在开发人员工具网络请求头中获取User-Agent信息需与站点Cookie配套使用"
hint="获取Cookie的浏览器对应的User-Agent"
persistent-hint
/>
</VCol>
</VRow>
@@ -200,7 +212,8 @@ async function updateSiteInfo() {
v-model="siteForm.limit_interval"
label="单位周期(秒)"
:rules="[numberValidator]"
hint="设定站点限流的单位周期单位为秒0为不限流"
hint="限流控制的单位周期时长"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -208,7 +221,8 @@ async function updateSiteInfo() {
v-model="siteForm.limit_count"
label="周期内访问次数"
:rules="[numberValidator]"
hint="设定单位周期内站点允许的访问次数0为不限制"
hint="单位周期内允许的访问次数"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -216,20 +230,17 @@ async function updateSiteInfo() {
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
:rules="[numberValidator]"
hint="设定单位周期内每次站点访问需间隔时间单位为秒0为不限制"
hint="每次访问需间隔的最小时间"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch v-model="siteForm.proxy" label="代理" hint="站点是否需要代理访问,需要设置好代理服务器信息" />
<VSwitch v-model="siteForm.proxy" label="代理" hint="使用代理服务器访问该站点" persistent-hint />
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="siteForm.render"
label="仿真"
hint="站点是否需要使用浏览器模拟访问,开启可以一定程度上提升连通性,但会大大增加站点请求时间"
/>
<VSwitch v-model="siteForm.render" label="仿真" hint="使用浏览器模拟真实访问该站点" persistent-hint />
</VCol>
</VRow>
</VForm>

View File

@@ -53,7 +53,7 @@ const subscribeForm = ref<Subscribe>({
last_update: '',
username: '',
current_priority: 0,
save_path: '',
save_path: undefined,
date: '',
show_edit_dialog: false,
})
@@ -301,7 +301,8 @@ onMounted(() => {
v-if="!props.default"
v-model="subscribeForm.keyword"
label="搜索关键词"
hint="定搜索关键词将使用此关键词搜索站点资源否则自动使用themoviedb中的名称搜索"
hint="定搜索站点时使用的关键词"
persistent-hint
/>
</VCol>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="2">
@@ -309,7 +310,8 @@ onMounted(() => {
v-model="subscribeForm.total_episode"
label="总集数"
:rules="[numberValidator]"
hint="手动设定总集数"
hint="剧集总集数"
persistent-hint
/>
</VCol>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="2">
@@ -317,19 +319,38 @@ onMounted(() => {
v-model="subscribeForm.start_episode"
label="开始集数"
:rules="[numberValidator]"
hint="只下载此集数及之后的集"
hint="开始订阅集数"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="4">
<VSelect v-model="subscribeForm.quality" label="质量" :items="qualityOptions" />
<VSelect
v-model="subscribeForm.quality"
label="质量"
:items="qualityOptions"
hint="订阅资源质量"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSelect v-model="subscribeForm.resolution" label="分辨率" :items="resolutionOptions" />
<VSelect
v-model="subscribeForm.resolution"
label="分辨率"
:items="resolutionOptions"
hint="订阅资源分辨率"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSelect v-model="subscribeForm.effect" label="特效" :items="effectOptions" />
<VSelect
v-model="subscribeForm.effect"
label="特效"
:items="effectOptions"
hint="订阅资源特效"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
@@ -337,14 +358,16 @@ onMounted(() => {
<VTextField
v-model="subscribeForm.include"
label="包含(关键字、正则式)"
hint="支持正则表达式,多个关键字用 | 分隔表示或"
hint="包含规则,支持正则表达式"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="subscribeForm.exclude"
label="排除(关键字、正则式)"
hint="支持正则表达式,多个关键字用 | 分隔表示或"
hint="排除规则,支持正则表达式"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -354,7 +377,8 @@ onMounted(() => {
chips
label="订阅站点"
multiple
hint="订阅选中的订阅站点,不选则订阅所有可订阅站点"
hint="订阅的站点范围,不选使用系统设置"
persistent-hint
/>
</VCol>
</VRow>
@@ -365,6 +389,7 @@ onMounted(() => {
:items="targetDirectories"
label="保存路径"
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
persistent-hint
/>
</VCol>
</VRow>
@@ -373,21 +398,24 @@ onMounted(() => {
<VSwitch
v-model="subscribeForm.best_version"
label="洗版"
hint="开启后不管媒体库是否存在,均会根据洗版优先级进行过滤下载,直到下载到了最高优先级的资源为止"
hint="根据洗版优先级进行洗版订阅"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSwitch
v-model="subscribeForm.search_imdbid"
label="使用 ImdbID 搜索"
hint="开启后将使用 ImdbID 搜索资源,搜索结果更精确,但不是所有站点都支持"
hint="开使用 ImdbID 精确搜索资源"
persistent-hint
/>
</VCol>
<VCol v-if="props.default" cols="12" md="4">
<VSwitch
v-model="subscribeForm.show_edit_dialog"
label="订阅时编辑更多规则"
hint="开启后将在添加订阅后弹出编辑订阅对话框,方便用户编辑订阅规则"
hint="添加订阅时显示此编辑订阅对话框"
persistent-hint
/>
</VCol>
</VRow>

View File

@@ -0,0 +1,89 @@
<script setup lang="ts">
import api from '@/api'
import { FileItem } from '@/api/types'
import { VTreeview } from 'vuetify/labs/VTreeview'
// 输入变量为默认路径
const props = defineProps({
root: {
type: String,
default: '/',
required: true,
},
})
// update:modelValue 事件
const emit = defineEmits(['update:modelValue'])
// 激活的目录
const activedDirs = ref<string[]>([])
// 打开的目录
const openedDirs = ref<string[]>([])
// 目录列表
const treeItems = ref<FileItem[]>([
{
name: '/',
path: props.root,
children: [],
type: '',
basename: props.root,
extension: '',
size: 0,
modify_time: 0,
},
])
// 拉取子目录
async function fetchDirs(item: any) {
return api
.get('/filebrowser/listdir?path=' + item.path)
.then((data: any) => {
item.children.push(...data)
})
.catch(err => console.warn(err))
}
// 获取选择的目录路径
const selectedPath = computed(() => {
if (activedDirs.value.length > 0) {
return activedDirs.value[0]
}
return ''
})
// 监听目录变化
watch(activedDirs, newVal => {
if (!newVal.length) return
emit('update:modelValue', selectedPath)
})
onMounted(() => {
fetchDirs(treeItems.value[0])
})
</script>
<template>
<VMenu :close-on-content-click="false" content-class="cursor-default">
<template v-slot:activator="{ props }">
<slot name="activator" :menuprops="props" />
</template>
<VTreeview
v-model:activated="activedDirs"
v-model:opened="openedDirs"
:items="treeItems"
:load-children="fetchDirs"
item-key="path"
item-title="name"
item-value="path"
item-type="unknown"
activatable
return-object
max-height="20rem"
expand-icon="mdi-folder"
collapse-icon="mdi-folder-open"
>
</VTreeview>
</VMenu>
</template>

View File

@@ -2,8 +2,6 @@
import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTitle.vue'
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue'
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
// Components
import Footer from '@/layouts/components/Footer.vue'
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import UserNofification from '@/layouts/components/UserNotification.vue'
@@ -11,9 +9,16 @@ import SearchBar from '@/layouts/components/SearchBar.vue'
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
import store from '@/store'
import { SystemNavMenus } from '@/router/menu'
import { NavMenu } from '@/@layouts/types'
// 从Vuex Store中获取superuser信息
const superUser = store.state.auth.superUser
// 根据分类获取菜单列表
const getMenuList = (header: string) => {
return SystemNavMenus.filter((item: NavMenu) => item.header === header && (!item.admin || superUser))
}
</script>
<template>
@@ -25,113 +30,44 @@ const superUser = store.state.auth.superUser
<IconBtn class="ms-n2 d-lg-none" @click="toggleVerticalOverlayNavActive(true)">
<VIcon icon="mdi-menu" />
</IconBtn>
<!-- 👉 Search Bar -->
<SearchBar />
<!-- 👉 Spacer -->
<VSpacer />
<!-- 👉 Shortcuts -->
<ShortcutBar v-if="superUser" />
<!-- 👉 Theme -->
<NavbarThemeSwitcher />
<!-- 👉 Notification -->
<UserNofification />
<!-- 👉 UserProfile -->
<UserProfile />
</div>
</template>
<template #vertical-nav-content>
<VerticalNavLink
:item="{
title: '仪表板',
icon: 'mdi-home-outline',
to: '/dashboard',
}"
/>
<VerticalNavLink v-for="item in getMenuList('开始')" :item="item" />
<!-- 👉 发现 -->
<VerticalNavSectionTitle
:item="{
heading: '发现',
}"
/>
<VerticalNavLink
:item="{
title: '推荐',
icon: 'mdi-table-star',
to: '/ranking',
}"
/>
<VerticalNavLink
:item="{
title: '资源搜索',
icon: 'mdi-magnify',
to: '/resource',
}"
/>
<VerticalNavLink v-for="item in getMenuList('发现')" :item="item" />
<!-- 👉 订阅 -->
<VerticalNavSectionTitle
:item="{
heading: '订阅',
}"
/>
<VerticalNavLink
:item="{
title: '电影',
icon: 'mdi-movie-check-outline',
to: '/subscribe-movie',
}"
/>
<VerticalNavLink
:item="{
title: '电视剧',
icon: 'mdi-television-classic',
to: '/subscribe-tv',
}"
/>
<VerticalNavLink
:item="{
title: '日历',
icon: 'mdi-calendar',
to: '/calendar',
}"
/>
<VerticalNavLink v-for="item in getMenuList('订阅')" :item="item" />
<!-- 👉 整理 -->
<VerticalNavSectionTitle
:item="{
heading: '整理',
}"
/>
<VerticalNavLink
:item="{
title: '正在下载',
icon: 'mdi-download-outline',
to: '/downloading',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '历史记录',
icon: 'mdi-history',
to: '/history',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '文件管理',
icon: 'mdi-folder-multiple-outline',
to: '/filemanager',
}"
/>
<VerticalNavLink v-for="item in getMenuList('整理')" :item="item" />
<!-- 👉 系统 -->
<VerticalNavSectionTitle
v-if="superUser"
@@ -139,37 +75,12 @@ const superUser = store.state.auth.superUser
heading: '系统',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '插件',
icon: 'mdi-apps',
to: '/plugins',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '站点管理',
icon: 'mdi-web',
to: '/site',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '设定',
icon: 'mdi-cog',
to: '/setting',
}"
/>
<VerticalNavLink v-for="item in getMenuList('系统')" :item="item" />
</template>
<template #after-vertical-nav-items />
<!-- 👉 Pages -->
<slot />
<!-- 👉 Footer -->
<template #footer>
<Footer />

View File

@@ -1,109 +1,39 @@
<script lang="ts" setup>
// 路由
const router = useRouter()
import * as Mousetrap from 'mousetrap'
import SearchBarView from '@/views/system/SearchBarView.vue'
// 搜索词
const searchWord = ref(null)
// 搜索弹窗
const searchDialog = ref(false)
// ref
const searchWordInput = ref<HTMLElement | null>(null)
// 当前的搜索类型 media/person
const searchType = ref('media')
// 搜索提示词列表
const searchHintList = ref<string[]>([])
// Search
function search() {
if (!searchWord.value) return
if (!searchHintList.value.includes(searchWord.value)) searchHintList.value.push(searchWord.value)
searchDialog.value = false
router.push({
path: '/browse/media/search',
query: {
title: searchWord.value,
type: searchType.value,
},
})
}
// 切换搜索类型
function switchSearchType() {
searchType.value = searchType.value === 'media' ? 'person' : 'media'
}
// 注册快捷键
Mousetrap.bind(['command+k', 'ctrl+k'], openSearchDialog)
// 打开搜索弹窗
function openSearchDialog() {
searchDialog.value = true
nextTick(() => {
searchWordInput.value?.focus()
})
return false
}
</script>
<template>
<!-- 👉 Search Button -->
<div class="d-flex align-center cursor-pointer" style="user-select: none">
<VDialog v-model="searchDialog" max-width="50rem" transition="dialog-top-transition">
<!-- Dialog Content -->
<VCard title="搜索">
<VCardText>
<VRow>
<VCol cols="12">
<VCombobox
ref="searchWordInput"
v-model="searchWord"
:items="searchHintList"
:prepend-inner-icon="searchType == 'person' ? 'mdi-account' : 'mdi-movie'"
:label="searchType == 'person' ? '搜索演员' : '搜索电影、电视剧'"
@keydown.enter="search"
@click:prepend-inner="switchSearchType"
clearable
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="tonal" @click="search"> 搜索 </VBtn>
</VCardActions>
</VCard>
</VDialog>
</div>
<!-- 👉 Search Icon -->
<IconBtn class="d-md-none" @click="openSearchDialog">
<VIcon icon="mdi-magnify" />
</IconBtn>
<!-- 👉 Search Textfield -->
<span class="w-full me-3">
<VCombobox
key="search_navbar"
v-model="searchWord"
:items="searchHintList"
class="d-none d-md-block text-disabled search-box"
density="compact"
variant="solo"
:prepend-inner-icon="searchType == 'person' ? 'mdi-account' : 'mdi-movie'"
:label="searchType == 'person' ? '搜索演员' : '搜索电影、电视剧'"
append-inner-icon="mdi-magnify"
single-line
hide-details
flat
rounded
@click:append-inner="search"
@click:prepend-inner="switchSearchType"
@keydown.enter="search"
/>
</span>
<div class="d-flex align-center cursor-pointer ms-lg-n2" style="user-select: none">
<IconBtn @click="openSearchDialog">
<VIcon icon="ri-search-line" />
</IconBtn>
<span class="d-none d-md-flex align-center text-disabled ms-2" @click="openSearchDialog">
<span class="me-3">搜索</span>
<span class="meta-key">K</span>
</span>
</div>
<!-- 搜索弹窗 -->
<SearchBarView v-model="searchDialog" v-if="searchDialog" @close="searchDialog = false" />
</template>
<style lang="scss">
.search-box div.v-input__control div[role='textbox'] {
border: 1px solid rgb(var(--v-theme-background));
<style type="scss" scoped>
.meta-key {
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 6px;
block-size: 1.75rem;
padding-block: 0.1rem;
padding-inline: 0.25rem;
}
</style>

View File

@@ -89,7 +89,7 @@ const avatar = store.state.auth.avatar
<VDivider class="my-2" />
<!-- 👉 Profile -->
<VListItem v-if="superUser" link to="setting">
<VListItem v-if="superUser" link @click="router.push('/setting?tab=account')">
<template #prepend>
<VIcon class="me-2" icon="mdi-account-outline" size="22" />
</template>

View File

@@ -1,22 +1,19 @@
import { VAceEditor } from 'vue3-ace-editor'
import { createApp } from 'vue'
import '@/@iconify/icons-bundle'
import ToastPlugin from 'vue-toast-notification'
import VuetifyUseDialog from 'vuetify-use-dialog'
import '@/@core/utils/compatibility'
import './ace-config'
import VueApexCharts from 'vue3-apexcharts'
import { removeEl } from './@core/utils/dom'
import '@/@iconify/icons-bundle'
import '@/plugins/webfontloader'
import App from '@/App.vue'
import vuetify from '@/plugins/vuetify'
import { loadFonts } from '@/plugins/webfontloader'
import router from '@/router'
import store from '@/store'
import '@core/scss/template/index.scss'
import '@layouts/styles/index.scss'
import '@styles/styles.scss'
import 'vue-toast-notification/dist/theme-bootstrap.css'
import { VAceEditor } from 'vue3-ace-editor'
import { createApp } from 'vue'
import { removeEl } from './@core/utils/dom'
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
import 'vue3-perfect-scrollbar/style.css'
import { VTreeview } from 'vuetify/labs/VTreeview'
import ToastPlugin from 'vue-toast-notification'
import VuetifyUseDialog from 'vuetify-use-dialog'
import VueApexCharts from 'vue3-apexcharts'
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import MediaCard from './components/cards/MediaCard.vue'
import PosterCard from './components/cards/PosterCard.vue'
@@ -25,13 +22,12 @@ import PersonCard from './components/cards/PersonCard.vue'
import MediaInfoCard from './components/cards/MediaInfoCard.vue'
import TorrentCard from './components/cards/TorrentCard.vue'
import MediaIdSelector from './components/misc/MediaIdSelector.vue'
import { fixArrayAt } from '@/@core/utils/compatibility'
// 修复低版本Safari等浏览器数组不支持at函数的问题
fixArrayAt()
// 加载字体
loadFonts()
import PathField from './components/input/PathField.vue'
import '@core/scss/template/index.scss'
import '@layouts/styles/index.scss'
import '@styles/styles.scss'
import 'vue-toast-notification/dist/theme-bootstrap.css'
import 'vue3-perfect-scrollbar/style.css'
// 创建Vue实例
const app = createApp(App)
@@ -48,6 +44,8 @@ app
.component('VMediaInfoCard', MediaInfoCard)
.component('VTorrentCard', TorrentCard)
.component('VMediaIdSelector', MediaIdSelector)
.component('VTreeview', VTreeview)
.component('VPathField', PathField)
// 注册插件
app

View File

@@ -158,7 +158,8 @@ async function loadDashboardConfig() {
}
}
// 是否拉升高度
isElevated.value = localStorage.getItem('MP_DASHBOARD_ELEVATED') === 'true'
const local_elevated = localStorage.getItem('MP_DASHBOARD_ELEVATED')
if (local_elevated) isElevated.value = local_elevated === 'true'
// 排序
if (orderConfig.value) {
sortDashboardConfigs()
@@ -179,7 +180,7 @@ function sortDashboardConfigs() {
}
// 设置项目
function saveDashboardConfig() {
async function saveDashboardConfig() {
// 启用配置
const data = JSON.stringify(enableConfig.value)
localStorage.setItem('MP_DASHBOARD', data)
@@ -190,12 +191,12 @@ function saveDashboardConfig() {
localStorage.setItem('MP_DASHBOARD_ELEVATED', isElevated.value.toString())
// 保存到服务端
try {
api.post('/user/config/Dashboard', data, {
await api.post('/user/config/Dashboard', data, {
headers: {
'Content-Type': 'application/json',
},
})
api.post('/user/config/DashboardOrder', order, {
await api.post('/user/config/DashboardOrder', order, {
headers: {
'Content-Type': 'application/json',
},
@@ -313,7 +314,7 @@ onBeforeMount(async () => {
</draggable>
<!-- 底部操作按钮 -->
<VFab icon="mdi-view-dashboard-edit" location="bottom end" size="x-large" fixed app appear @click="dialog = true" />
<VFab icon="mdi-view-dashboard-edit" location="bottom" size="x-large" fixed app appear @click="dialog = true" />
<!-- 弹窗根据配置生成选项 -->
<VDialog v-model="dialog" max-width="35rem" scrollable>
@@ -339,7 +340,7 @@ onBeforeMount(async () => {
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch v-model="isElevated" label="高度拉升" />
<VSwitch v-model="isElevated" label="自适应组件高度" />
</VCol>
</VRow>
</VCardText>

View File

@@ -8,6 +8,7 @@ import router from '@/router'
import logo from '@images/logo.png'
import { useTheme } from 'vuetify'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { urlBase64ToUint8Array } from '@/@core/utils/navigator'
const { global: globalTheme } = useTheme()
@@ -89,9 +90,33 @@ async function setTheme() {
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
}
// 订阅推送通知
async function subscribeForPushNotifications() {
if ('serviceWorker' in navigator && 'PushManager' in window) {
const registration = await navigator.serviceWorker.ready
// 获取订阅信息
const subscription = await registration.pushManager.getSubscription().then(function (subscription) {
if (subscription === null) {
const convertedVapidKey = urlBase64ToUint8Array(import.meta.env.VITE_PUBLIC_VAPID_KEY)
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey,
})
} else {
return subscription
}
})
// 发送订阅请求
await api.post('/message/subscribe', subscription)
}
}
// 登录后处理
async function afterLogin() {
// 生效主题配置
await setTheme()
// 订阅推送通知
await subscribeForPushNotifications()
// 跳转到首页或回原始页面
router.push(store.state.auth.originalPath ?? '/')
}

View File

@@ -65,7 +65,7 @@ function startLoadingProgress() {
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
if (progressEventSource.value) progressEventSource.value?.close()
}
// 设置视图类型
@@ -82,23 +82,28 @@ async function fetchData() {
dataList.value = await api.get('search/last')
} else {
startLoadingProgress()
let result: { [key: string]: any }
// 优先按TMDBID精确查询
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:') || keyword?.startsWith('bangumi:')) {
const result: { [key: string]: any } = await api.get(`search/media/${keyword}`, {
result = await api.get(`search/media/${keyword}`, {
params: {
mtype: type,
area,
season,
},
})
if (result.success) {
dataList.value = result.data
} else {
errorDescription.value = result.message
}
} else {
// 按标题模糊查询
dataList.value = await api.get(`search/title/${keyword}`)
result = await api.get(`search/title`, {
params: {
keyword,
},
})
}
if (result && result.success) {
dataList.value = result.data
} else if (result && result.message) {
errorDescription.value = result.message
}
stopLoadingProgress()
// 从浏览器历史中删除当前搜索
@@ -116,6 +121,11 @@ async function fetchData() {
onMounted(() => {
fetchData()
})
// 卸载时停止加载进度
onUnmounted(() => {
stopLoadingProgress()
})
</script>
<template>
@@ -133,21 +143,12 @@ onMounted(() => {
<VFab
v-if="viewType === 'list'"
icon="mdi-view-grid"
location="bottom end"
location="bottom"
size="x-large"
fixed
app
appear
@click="setViewType('card')"
/>
<VFab
v-else
icon="mdi-view-list"
location="bottom end"
size="x-large"
fixed
app
appear
@click="setViewType('list')"
/>
<VFab v-else icon="mdi-view-list" location="bottom" size="x-large" fixed app appear @click="setViewType('list')" />
</template>

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import { useRoute } from 'vue-router'
import router from '@/router'
import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue'
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
@@ -10,70 +11,27 @@ import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
import { SettingTabs } from '@/router/menu'
const route = useRoute()
const activeTab = ref(route.params.tab)
const activeTab = ref(route.query.tab)
// tabs
const tabs = [
{
title: '用户',
icon: 'mdi-account',
tab: 'account',
},
{
title: '连接',
icon: 'mdi-server-network',
tab: 'system',
},
{
title: '目录',
icon: 'mdi-folder',
tab: 'directory',
},
{
title: '站点',
icon: 'mdi-web',
tab: 'site',
},
{
title: '搜索',
icon: 'mdi-magnify',
tab: 'search',
},
{
title: '订阅',
icon: 'mdi-rss',
tab: 'subscribe',
},
{
title: '服务',
icon: 'mdi-list-box',
tab: 'service',
},
{
title: '通知',
icon: 'mdi-bell',
tab: 'notification',
},
{
title: '词表',
icon: 'mdi-file-word-box',
tab: 'words',
},
{
title: '关于',
icon: 'mdi-information',
tab: 'about',
},
]
function jumpTab(tab: string) {
router.push('/setting?tab=' + tab)
}
</script>
<template>
<div>
<VTabs v-model="activeTab" show-arrows class="v-tabs-pill">
<VTab v-for="item in tabs" :key="item.icon" :value="item.tab">
<VTab
v-for="item in SettingTabs"
:key="item.icon"
:value="item.tab"
@click="jumpTab(item.tab)"
selected-class="v-slide-group-item--active v-tab--selected"
>
<VIcon size="20" start :icon="item.icon" />
{{ item.title }}
</VTab>

View File

@@ -1,29 +1,27 @@
<script setup lang="ts">
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
import router from '@/router'
import { SubscribeMovieTabs } from '@/router/menu'
const route = useRoute()
// 标签页
const tabs = [
{
title: '我的订阅',
tab: 'mysub',
},
{
title: '热门订阅',
tab: 'popular',
},
]
// 订阅ID参数
const subId = ref(route.query.id as string)
// 当前标签
const activeTab = ref(route.params.tab)
const activeTab = ref(route.query.tab)
// 跳转tab
function jumpTab(tab: string) {
router.push('/subscribe-movie?tab=' + tab)
}
</script>
<template>
<div>
<VTabs v-model="activeTab">
<VTab v-for="item in tabs" :value="item.tab">
<VTab v-for="item in SubscribeMovieTabs" :value="item.tab" @click="jumpTab(item.tab)">
<span class="mx-5">{{ item.title }}</span>
</VTab>
</VTabs>
@@ -31,12 +29,12 @@ const activeTab = ref(route.params.tab)
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem value="mysub">
<transition name="fade-slide" appear>
<SubscribeListView type="电影" />
<SubscribeListView type="电影" :subid="subId" />
</transition>
</VWindowItem>
<VWindowItem value="popular">
<transition name="fade-slide" appear>
<SubscribePopularView type="电影" />
<SubscribePopularView type="电影" :subid="subId" />
</transition>
</VWindowItem>
</VWindow>

View File

@@ -1,29 +1,26 @@
<script setup lang="ts">
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
import router from '@/router'
import { SubscribeTvTabs } from '@/router/menu'
const route = useRoute()
// 标签页
const tabs = [
{
title: '我的订阅',
tab: 'mysub',
},
{
title: '热门订阅',
tab: 'popular',
},
]
const activeTab = ref(route.query.tab)
// 当前标签
const activeTab = ref(route.params.tab)
// 订阅ID参数
const subId = ref(route.query.id as string)
// 跳转tab
function jumpTab(tab: string) {
router.push('/subscribe-tv?tab=' + tab)
}
</script>
<template>
<div>
<VTabs v-model="activeTab">
<VTab v-for="item in tabs" :value="item.tab">
<VTab v-for="item in SubscribeTvTabs" :value="item.tab" @click="jumpTab(item.tab)">
<span class="mx-5">{{ item.title }}</span>
</VTab>
</VTabs>
@@ -31,12 +28,12 @@ const activeTab = ref(route.params.tab)
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem value="mysub">
<transition name="fade-slide" appear>
<SubscribeListView type="电视剧" />
<SubscribeListView type="电视剧" :subid="subId" />
</transition>
</VWindowItem>
<VWindowItem value="popular">
<transition name="fade-slide" appear>
<SubscribePopularView type="电视剧" />
<SubscribePopularView type="电视剧" :subid="subId" />
</transition>
</VWindowItem>
</VWindow>

View File

@@ -4,12 +4,12 @@
* webfontloader documentation: https://github.com/typekit/webfontloader
*/
export async function loadFonts() {
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */'webfontloader')
;(async function loadFonts() {
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */ 'webfontloader')
webFontLoader.load({
google: {
families: ['Inter:100,200,300,400,500,600,700&display=swap'],
},
})
}
})()

View File

@@ -10,8 +10,7 @@ const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
scrollBehavior(to, from, savedPosition) {
// 如果页面有缓存那么恢复其位置, 否则始终滚动到顶部
if (to.meta.keepAlive && savedPosition)
return savedPosition
if (to.meta.keepAlive && savedPosition) return savedPosition
return { top: 0 }
},
routes: [
@@ -21,14 +20,14 @@ const router = createRouter({
component: () => import('../layouts/default.vue'),
children: [
{
path: 'dashboard',
path: '/dashboard',
component: () => import('../pages/dashboard.vue'),
meta: {
requiresAuth: true,
},
},
{
path: 'ranking',
path: '/ranking',
component: () => import('../pages/ranking.vue'),
meta: {
keepAlive: true,
@@ -36,63 +35,63 @@ const router = createRouter({
},
},
{
path: 'resource',
path: '/resource',
component: () => import('../pages/resource.vue'),
meta: {
requiresAuth: true,
},
},
{
path: 'subscribe-movie',
path: '/subscribe-movie',
component: () => import('../pages/subscribe-movie.vue'),
meta: {
requiresAuth: true,
},
},
{
path: 'subscribe-tv',
path: '/subscribe-tv',
component: () => import('../pages/subscribe-tv.vue'),
meta: {
requiresAuth: true,
},
},
{
path: 'calendar',
path: '/calendar',
component: () => import('../pages/calendar.vue'),
meta: {
requiresAuth: true,
},
},
{
path: 'downloading',
path: '/downloading',
component: () => import('../pages/downloading.vue'),
meta: {
requiresAuth: true,
},
},
{
path: 'history',
path: '/history',
component: () => import('../pages/history.vue'),
meta: {
requiresAuth: true,
},
},
{
path: 'site',
path: '/site',
component: () => import('../pages/site.vue'),
meta: {
requiresAuth: true,
},
},
{
path: 'plugins',
path: '/plugins',
component: () => import('../pages/plugin.vue'),
meta: {
requiresAuth: true,
},
},
{
path: 'setting',
path: '/setting',
component: () => import('../pages/setting.vue'),
meta: {
requiresAuth: true,
@@ -165,8 +164,7 @@ router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
}
else {
} else {
startNProgress()
next()
}

221
src/router/menu.ts Normal file
View File

@@ -0,0 +1,221 @@
// 导般菜单
export const SystemNavMenus = [
{
title: '仪表板',
icon: 'mdi-home-outline',
to: '/dashboard',
header: '开始',
admin: false,
},
{
title: '推荐',
icon: 'mdi-table-star',
to: '/ranking',
header: '发现',
admin: false,
},
{
title: '资源搜索',
icon: 'mdi-magnify',
to: '/resource',
header: '发现',
admin: false,
},
{
title: '电影',
icon: 'mdi-movie-roll',
to: '/subscribe-movie?tab=mysub',
header: '订阅',
admin: false,
},
{
title: '电视剧',
icon: 'mdi-television-classic',
to: '/subscribe-tv?tab=mysub',
header: '订阅',
admin: false,
},
{
title: '日历',
icon: 'mdi-calendar',
to: '/calendar',
header: '订阅',
admin: false,
},
{
title: '正在下载',
icon: 'mdi-download-outline',
to: '/downloading',
header: '整理',
admin: false,
},
{
title: '历史记录',
icon: 'mdi-history',
to: '/history',
header: '整理',
admin: true,
},
{
title: '文件管理',
icon: 'mdi-folder-multiple-outline',
to: '/filemanager',
header: '整理',
admin: true,
},
{
title: '插件',
icon: 'mdi-apps',
to: '/plugins?tab=installed',
header: '系统',
admin: true,
},
{
title: '站点管理',
icon: 'mdi-web',
to: '/site',
header: '系统',
admin: true,
},
{
title: '设定',
icon: 'mdi-cog',
to: '/setting',
header: '系统',
admin: true,
},
]
// 常用菜单功能
export const UserfulMenus = [
{
title: '搜索设置',
icon: 'mdi-magnify',
to: 'setting?tab=search',
},
{
title: '订阅设置',
icon: 'mdi-rss',
to: 'setting?tab=subscribe',
},
{
title: '服务',
icon: 'mdi-list-box',
to: 'setting?tab=service',
},
{
title: '词表',
icon: 'mdi-file-word-box',
to: 'setting?tab=words',
},
{
title: '历史记录',
icon: 'mdi-history',
to: 'history',
},
]
// 设定标签页
export const SettingTabs = [
{
title: '用户',
icon: 'mdi-account',
tab: 'account',
description: '个人信息、用户管理、修改密码、双重认证',
},
{
title: '连接',
icon: 'mdi-server-network',
tab: 'system',
description: '下载器Qbittorrent、Transmission、媒体服务器Emby、Jellyfin、Plex',
},
{
title: '目录',
icon: 'mdi-folder',
tab: 'directory',
description: '下载目录、媒体库目录、整理模式',
},
{
title: '站点',
icon: 'mdi-web',
tab: 'site',
description: '站点同步、下载优先规则、站点重置',
},
{
title: '搜索',
icon: 'mdi-magnify',
tab: 'search',
description: '媒体数据源TheMovieDb、豆瓣、Bangumi、搜索站点、搜索优先级、默认过滤规则',
},
{
title: '订阅',
icon: 'mdi-rss',
tab: 'subscribe',
description: '订阅站点、订阅模式、订阅优先级、洗版优先级、默认过滤规则',
},
{
title: '服务',
icon: 'mdi-list-box',
tab: 'service',
description: '定时作业',
},
{
title: '通知',
icon: 'mdi-bell',
tab: 'notification',
description: '通知渠道微信、Telegram、Slack、SynologyChat、VoceChat、消息类型',
},
{
title: '词表',
icon: 'mdi-file-word-box',
tab: 'words',
description: '自定义识别词、自定义制作组/字幕组、自定义占位符、文件整理屏蔽词',
},
{
title: '关于',
icon: 'mdi-information',
tab: 'about',
},
]
// 电影订阅标签页
export const SubscribeMovieTabs = [
{
title: '我的订阅',
tab: 'mysub',
icon: 'mdi-movie-roll',
},
{
title: '热门订阅',
tab: 'popular',
icon: 'mdi-movie-roll',
},
]
// 电视剧订阅标签页
export const SubscribeTvTabs = [
{
title: '我的订阅',
tab: 'mysub',
icon: 'mdi-television-classic',
},
{
title: '热门订阅',
tab: 'popular',
icon: 'mdi-television-classic',
},
]
// 插件标签页
export const PluginTabs = [
{
title: '我的插件',
tab: 'installed',
icon: 'mdi-puzzle',
},
{
title: '插件市场',
tab: 'market',
icon: 'mdi-store',
},
]

39
src/service-worker.ts Normal file
View File

@@ -0,0 +1,39 @@
import { createHandlerBoundToURL, cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
import { NavigationRoute, registerRoute } from 'workbox-routing'
import { clientsClaim } from 'workbox-core'
declare let self: ServiceWorkerGlobalScope
cleanupOutdatedCaches()
// self.__WB_MANIFEST is default injection point
precacheAndRoute(self.__WB_MANIFEST)
// to allow work offline
registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'), { denylist: [/^\/api/] }))
// 通知选项
const options = {
icon: '/logo.png',
}
// 监听 push 事件,显示通知
self.addEventListener('push', function (e) {
if (!e.data) {
return
}
// 解析获取推送消息
let payload = e.data.json()
// 根据推送消息生成桌面通知并展现出来
let promise = self.registration.showNotification(payload.title, {
body: payload.body,
icon: payload.icon ?? options.icon,
data: {
url: payload.url,
},
})
e.waitUntil(promise)
})
self.skipWaiting()
clientsClaim()

View File

@@ -16,7 +16,7 @@ const variableTheme = controlledComputed(
)
// 定时器
let refreshTimer: NodeJS.Timer | null = null
let refreshTimer: NodeJS.Timeout | null = null
// 时间序列
const series = ref([

View File

@@ -17,7 +17,7 @@ const variableTheme = controlledComputed(
)
// 定时器
let refreshTimer: NodeJS.Timer | null = null
let refreshTimer: NodeJS.Timeout | null = null
// 时间序列
const series = ref([

View File

@@ -10,7 +10,7 @@ const headers = ['进程ID', '进程名称', '运行时间', '内存占用']
const processList = ref<Process[]>([])
// 定时器
let refreshTimer: NodeJS.Timer | null = null
let refreshTimer: NodeJS.Timeout | null = null
// 调用API加载数据
async function loadProcessList() {

View File

@@ -6,7 +6,7 @@ import type { ScheduleInfo } from '@/api/types'
const schedulerList = ref<ScheduleInfo[]>([])
// 定时器
let refreshTimer: NodeJS.Timer | null = null
let refreshTimer: NodeJS.Timeout | null = null
// 调用API加载定时服务列表
async function loadSchedulerList() {

View File

@@ -4,7 +4,7 @@ import api from '@/api'
import type { DownloaderInfo } from '@/api/types'
// 定时器
let refreshTimer: NodeJS.Timer | null = null
let refreshTimer: NodeJS.Timeout | null = null
// 下载器信息
const downloadInfo = ref<DownloaderInfo>({

View File

@@ -71,7 +71,34 @@ function initOptions(data: Context) {
// 对季过滤选项进行排序
const sortSeasonFilterOptions = computed(() => {
return seasonFilterOptions.value.sort((a, b) => {
// 按字符串降序排序
// 按季,集降序排序
const parseSeasonEpisode = (str: string) => {
const seasonRangeMatch = str.match(/S(\d+)(?:-S(\d+))?/);
const episodeRangeMatch = str.match(/E(\d+)(?:-E(\d+))?/);
return {
seasonStart : seasonRangeMatch?.[1] ? parseInt(seasonRangeMatch[1]) : 0,
seasonEnd : seasonRangeMatch?.[2] ? parseInt(seasonRangeMatch[2]) : 0,
episodeStart : episodeRangeMatch?.[1] ? parseInt(episodeRangeMatch[1]) : 0,
episodeEnd : episodeRangeMatch?.[2] ? parseInt(episodeRangeMatch[2]) : 0
}
}
const parsedA = parseSeasonEpisode(a)
const parsedB = parseSeasonEpisode(b)
// 先按季降序排序
if (parsedB.seasonStart !== parsedA.seasonStart) {
return parsedB.seasonStart - parsedA.seasonStart
}
if (parsedB.seasonEnd !== parsedA.seasonEnd) {
return parsedB.seasonEnd - parsedA.seasonEnd
}
// 按集降序排序
if (parsedB.episodeStart !== parsedA.episodeStart) {
return parsedB.episodeStart - parsedA.episodeStart
}
if (parsedB.episodeEnd !== parsedA.episodeEnd) {
return parsedB.episodeEnd - parsedA.episodeEnd
}
// 兜底
return b.localeCompare(a)
})
})
@@ -112,7 +139,10 @@ watchEffect(() => {
groupedDataList.value?.forEach(value => {
if (value.length > 0) {
const matchData = value.filter(data => {
const { meta_info, torrent_info } = data
const {
meta_info,
torrent_info,
} = data
// 季、制作组、视频编码
return (
// 站点过滤

View File

@@ -9,6 +9,8 @@ import noImage from '@images/logos/plugin.png'
import { useDisplay } from 'vuetify'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useDefer } from '@/@core/utils/dom'
import router from '@/router'
import { PluginTabs } from '@/router/menu'
const route = useRoute()
@@ -19,19 +21,10 @@ const display = useDisplay()
let deferApp = (_: number) => true
// 当前标签
const activeTab = ref(route.params.tab)
const activeTab = ref(route.query.tab)
// 标签页
const tabs = [
{
title: '我的插件',
tab: 'myplugin',
},
{
title: '插件市场',
tab: 'pluginmarket',
},
]
// 插件ID参数
const pluginId = ref(route.query.id)
// 当前排序字段
const activeSort = ref(null)
@@ -320,24 +313,36 @@ function handleRepoUrl(url: string | undefined) {
return url.replace('https://github.com/', '').replace('https://raw.githubusercontent.com/', '')
}
// 跳转tab
function jumpTab(tab: string) {
router.push('/plugins?tab=' + tab)
}
// 加载时获取数据
onBeforeMount(async () => {
await refreshData()
getPluginStatistics()
if (activeTab.value != 'market' && pluginId.value) {
// 找到这个插件
const plugin = dataList.value.find(item => item.id === pluginId.value)
if (plugin) {
plugin.page_open = true
}
}
})
</script>
<template>
<div>
<VTabs v-model="activeTab">
<VTab v-for="item in tabs" :value="item.tab">
<VTab v-for="item in PluginTabs" :value="item.tab" @click="jumpTab(item.tab)">
<span class="mx-5">{{ item.title }}</span>
</VTab>
</VTabs>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<!-- 我的插件 -->
<VWindowItem value="myplugin">
<VWindowItem value="installed">
<transition name="fade-slide" appear>
<div>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
@@ -363,7 +368,7 @@ onBeforeMount(async () => {
</transition>
</VWindowItem>
<!-- 插件市场 -->
<VWindowItem value="pluginmarket">
<VWindowItem value="market">
<transition name="fade-slide" appear>
<div>
<LoadingBanner v-if="!isAppMarketLoaded" class="mt-12" />
@@ -437,7 +442,7 @@ onBeforeMount(async () => {
<VFab
icon="mdi-magnify"
color="info"
location="bottom end"
location="bottom"
class="mb-2"
size="x-large"
fixed

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import PullRefresh from 'pull-refresh-vue3'
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'
import api from '@/api'
import type { DownloadingInfo } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
@@ -7,7 +7,7 @@ import DownloadingCard from '@/components/cards/DownloadingCard.vue'
import store from '@/store'
// 定时器
let refreshTimer: NodeJS.Timer | null = null
let refreshTimer: NodeJS.Timeout | null = null
// 数据列表
const dataList = ref<DownloadingInfo[]>([])
@@ -20,8 +20,7 @@ async function fetchData() {
try {
dataList.value = await api.get('download/')
isRefreshed.value = true
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -41,10 +40,8 @@ const filteredDataList = computed(() => {
// 从Vuex Store中获取用户信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
if (superUser)
return dataList.value
else
return dataList.value.filter(data => data.userid === userName || data.username === userName)
if (superUser) return dataList.value
else return dataList.value.filter(data => data.userid === userName || data.username === userName)
})
// 加载时获取数据
@@ -67,23 +64,10 @@ onUnmounted(() => {
</script>
<template>
<LoadingBanner
v-if="!isRefreshed"
class="mt-12"
/>
<PullRefresh
v-model="loading"
@refresh="onRefresh"
>
<div
v-if="filteredDataList.length > 0"
class="grid gap-3 grid-downloading-card"
>
<DownloadingCard
v-for="data in filteredDataList"
:key="data.hash"
:info="data"
/>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VPullToRefresh v-model="loading" @load="onRefresh" :pull-down-threshold="64">
<div v-if="filteredDataList.length > 0" class="grid gap-3 grid-downloading-card">
<DownloadingCard v-for="data in filteredDataList" :key="data.hash" :info="data" />
</div>
<NoDataFound
v-if="filteredDataList.length === 0 && isRefreshed"
@@ -91,5 +75,5 @@ onUnmounted(() => {
error-title="没有任务"
error-description="正在下载的任务将会显示在这里"
/>
</PullRefresh>
</VPullToRefresh>
</template>

View File

@@ -1,15 +1,19 @@
<script setup lang="ts">
import { debounce } from 'lodash'
import { ref, unref } from 'vue'
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { TransferHistory } from '@/api/types'
import ReorganizeDialog from '@/components/dialog/ReorganizeDialog.vue'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useRoute } from 'vue-router'
import router from '@/router'
// 提示框
const $toast = useToast()
// 路由
const route = useRoute()
// 重新整理对话框
const redoDialog = ref(false)
@@ -69,7 +73,7 @@ const pageRange = [
const dataList = ref<TransferHistory[]>([])
// 搜索
const search = ref()
const search = ref(route.query.search as string)
// 搜索提示词列表
const searchHintList = ref<string[]>([])
@@ -81,10 +85,10 @@ const loading = ref(false)
const totalItems = ref(0)
// 每页条数
const itemsPerPage = ref(50)
const itemsPerPage = ref<number>(ensureNumber(route.query.itemsPerPage, 50))
// 当前页码
const currentPage = ref(1)
const currentPage = ref<number>(ensureNumber(route.query.currentPage, 1))
// 进度条
const progressDialog = ref(false)
@@ -113,8 +117,8 @@ const TransferDict: { [key: string]: string } = {
// 分页提示
const pageTip = computed(() => {
const begin = unref(itemsPerPage) * (unref(currentPage) - 1) + 1
const end = unref(itemsPerPage) * unref(currentPage) === -1 ? 'ALL' : unref(itemsPerPage) * unref(currentPage)
const begin = itemsPerPage.value * (currentPage.value - 1) + 1
const end = itemsPerPage.value * currentPage.value === -1 ? 'ALL' : itemsPerPage.value * currentPage.value
return {
begin,
end,
@@ -123,7 +127,7 @@ const pageTip = computed(() => {
// 分页总数
const totalPage = computed(() => {
const total = Math.ceil(unref(totalItems) / unref(itemsPerPage))
const total = Math.ceil(totalItems.value / itemsPerPage.value)
return total
})
@@ -131,7 +135,7 @@ const totalPage = computed(() => {
watch(
[() => currentPage.value, () => itemsPerPage.value, () => search.value],
debounce(async () => {
await fetchData()
reloadPage()
}, 1000),
)
@@ -272,6 +276,16 @@ async function retransferBatch() {
redoDialog.value = true
}
// 整理完成
function transferDone() {
redoDialog.value = false
// 清空当前操作记录
currentHistory.value = undefined
selected.value = []
// 刷新
fetchData()
}
// 弹出菜单
const dropdownItems = ref([
{
@@ -298,6 +312,38 @@ const dropdownItems = ref([
},
])
// 添加url参数
function addUrlQuery(url: string, name: string, value: any) {
if (!url || !name || !value) return url
const separator = url.includes('?') ? '&' : '?'
return url + separator + name + '=' + encodeURIComponent(value)
}
// 重载页面
function reloadPage() {
let url = '/history'
if (search.value) {
url = addUrlQuery(url, 'search', search.value)
}
if (itemsPerPage.value) {
url = addUrlQuery(url, 'itemsPerPage', itemsPerPage.value)
}
if (currentPage.value) {
url = addUrlQuery(url, 'currentPage', currentPage.value)
}
router.push(url)
}
// 确保值为number类型
function ensureNumber(value: any, defaultValue: number = 0) {
value = Number(value)
// 如果不是数字
if (value !== value) {
value = defaultValue
}
return value
}
// 初始加载数据
onMounted(fetchData)
</script>
@@ -414,6 +460,32 @@ onMounted(fetchData)
</VPagination>
</div>
</VCard>
<!-- 底部操作按钮 -->
<span>
<VFab
v-if="selected.length > 0"
icon="mdi-trash-can-outline"
color="error"
location="bottom"
size="x-large"
fixed
app
appear
@click="removeHistoryBatch"
/>
<VFab
v-if="selected.length > 0"
class="mb-16"
icon="mdi-redo-variant"
location="bottom"
size="x-large"
fixed
app
appear
@click="retransferBatch"
/>
</span>
<!-- 底部弹窗 -->
<VBottomSheet v-model="deleteConfirmDialog" inset>
<VCard class="text-center rounded-t">
@@ -440,43 +512,9 @@ onMounted(fetchData)
v-if="redoDialog"
v-model="redoDialog"
:logids="redoIds"
@done="
() => {
redoDialog = false
// 清空当前操作记录
currentHistory = undefined
selected = []
// 刷新
fetchData()
}
"
@done="transferDone"
@close="redoDialog = false"
/>
<!-- 底部操作按钮 -->
<span>
<VFab
v-if="selected.length > 0"
icon="mdi-trash-can-outline"
color="error"
location="bottom end"
size="x-large"
fixed
app
appear
@click="removeHistoryBatch"
/>
<VFab
v-if="selected.length > 0"
class="mb-2"
icon="mdi-redo-variant"
location="bottom end"
size="x-large"
fixed
app
appear
@click="retransferBatch"
/>
</span>
</template>
<style lang="scss">

View File

@@ -5,7 +5,7 @@ import draggable from 'vuedraggable'
import { VRow } from 'vuetify/lib/components/index.mjs'
import api from '@/api'
import { MediaDirectory } from '@/api/types'
import MediaDirectoryCard from '@/components/cards/MediaDirectoryCard.vue'
import DirectoryCard from '@/components/cards/DirectoryCard.vue'
// 媒体库设置项
const transferSettings = ref({
@@ -223,10 +223,11 @@ onMounted(() => {
:component-data="{ 'class': 'grid gap-3 grid-directory-card' }"
>
<template #item="{ element }">
<MediaDirectoryCard
<DirectoryCard
type="download"
:directory="element"
:categories="mediaCategories"
@update:modelValue="(value: string) => (element.path = value)"
@close="downloadCardClose(element.name)"
/>
</template>
@@ -256,10 +257,11 @@ onMounted(() => {
:component-data="{ 'class': 'grid gap-3 grid-directory-card' }"
>
<template #item="{ element }">
<MediaDirectoryCard
<DirectoryCard
type="library"
:directory="element"
:categories="mediaCategories"
@update:modelValue="(value: string) => (element.path = value)"
@close="libraryCardClose(element.name)"
/>
</template>
@@ -287,7 +289,8 @@ onMounted(() => {
v-model="transferSettings.TRANSFER_TYPE"
:items="transferTypeItems"
label="整理方式"
hint="硬链接需要确保下载目录媒体库目录不跨盘、不跨共享目录、不分别映射rclone需要手动在容器中完成配置且配置名为`MP`"
hint="文件从下载目录整理到媒体库目录的操作方式"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -295,14 +298,16 @@ onMounted(() => {
v-model="transferSettings.OVERWRITE_MODE"
:items="overwriteModeItems"
label="覆盖模式"
hint="从不覆盖:不覆盖已存在的文件;按大小覆盖:大文件将覆盖小文件;总是覆盖:总是覆盖已存在的文件;仅保留最新版本:保留最新版本的文件,删除其它版本的文件"
hint="媒体库中同名文件已存在时的覆盖方式"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="transferSettings.TRANSFER_SAME_DISK"
label="同盘/同根目录优先"
hint="开启后优先整理到与下载目录同一磁盘/同一根路径的媒体库目录中"
hint="优先整理到与下载目录同一磁盘/同一根路径的媒体库目录中"
persistent-hint
/>
</VCol>
</VRow>

View File

@@ -195,7 +195,8 @@ onMounted(() => {
chips
:items="NotificationChannels"
label="当前使用通知渠道"
hint="选中的渠道才会按消息类型的设定发送消息"
hint="消息通知渠道总开关"
persistent-hint
/>
</VCol>
</VRow>
@@ -216,42 +217,48 @@ onMounted(() => {
<VTextField
v-model="notificationSettings.WECHAT_CORPID"
label="企业ID"
hint="登录企业微信后台,在 https://work.weixin.qq.com/wework_admin/frame#profile 中查看"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.WECHAT_APP_SECRET"
label="应用Secret"
hint="在企业微信中创建应用查看应用的Secret"
hint="企业微信后台企业信息中的企业ID"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.WECHAT_APP_ID"
label="应用 AgentId"
hint="企业微信中创建应用,查看应用的AgentId"
hint="企业微信自建应用的AgentId"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.WECHAT_APP_SECRET"
label="应用Secret"
hint="企业微信自建应用的Secret"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.WECHAT_PROXY"
label="代理地址"
hint="由于微信官方限制2022年6月20日后创建的企业微信应用需要有固定的公网IP地址并加入IP白名单后才能接收消息使用有固定公网IP的代理服务器转发可解决该问题代理服务器需自行搭建搭建方法参考项目主页说明,不使用代理保留默认值"
hint="微信消息的转发代理地址2022年6月20日后创建的自建应用需要,不使用代理时需要保留默认值"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.WECHAT_TOKEN"
label="Token"
hint="微信企业应用管理后台-接收消息设置页面生成"
hint="微信企业自建应用->API接收消息配置中的Token"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.WECHAT_ENCODING_AESKEY"
label="EncodingAESKey"
hint="微信企业应用管理后台-接收消息设置页面生成所有信息填入完成后保存然后再在企业微信应用消息接收服务中输入回调地址http(s)://domain:port/api/v1/message/"
hint="微信企业自建应用->API接收消息配置中的EncodingAESKey"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -259,7 +266,8 @@ onMounted(() => {
v-model="notificationSettings.WECHAT_ADMINS"
label="管理员白名单"
placeholder="多个用,分隔"
hint="只有在白名单中的用户才能使用菜单管理功能,不填写则所有用户都能使用,菜单会自动生成,不需要手动创建"
hint="可使用管理菜单及命令的用户ID列表多个ID使用,分隔"
persistent-hint
/>
</VCol>
</VRow>
@@ -272,14 +280,16 @@ onMounted(() => {
<VTextField
v-model="notificationSettings.TELEGRAM_TOKEN"
label="Bot Token"
hint="Telegram机器人的token关注BotFather创建机器人并获取token格式123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
hint="Telegram机器人token格式123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationSettings.TELEGRAM_CHAT_ID"
label="Chat ID"
hint="接受消息通知的用户、群组或频道Chat ID,关注@getidsbot获取"
hint="接受消息通知的用户、群组或频道Chat ID"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -287,7 +297,8 @@ onMounted(() => {
v-model="notificationSettings.TELEGRAM_USERS"
label="用户白名单"
placeholder="多个用,分隔"
hint="只有在白名单中的用户才能使用Telegram机器人不填写则所有用户都能使用,多个用户用英文,分隔"
hint="使用Telegram机器人的用户ID清单多个用户用,分隔,不填写则所有用户都能使用"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -295,7 +306,8 @@ onMounted(() => {
v-model="notificationSettings.TELEGRAM_ADMINS"
label="管理员白名单"
placeholder="多个用,分隔"
hint="只有在白名单中的用户才能使用管理功能,不填写则所有用户都能使用,多个用户用英文,分隔。菜单会自动生成,不需要手动创建"
hint="可使用管理菜单及命令的用户ID列表多个ID使用,分隔"
persistent-hint
/>
</VCol>
</VRow>
@@ -309,7 +321,8 @@ onMounted(() => {
v-model="notificationSettings.SLACK_OAUTH_TOKEN"
label="Slack Bot User OAuth Token"
placeholder="xoxb-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
hint="在 https://api.slack.com/apps 中创建应用,查看OAuth & Permissions页面中的Bot User OAuth Token"
hint="Slack应用`OAuth & Permissions`页面中的`Bot User OAuth Token`"
persistent-hint
/>
</VCol>
<VCol cols="12" md="5">
@@ -317,7 +330,8 @@ onMounted(() => {
v-model="notificationSettings.SLACK_APP_TOKEN"
label="Slack App-Level Token"
placeholder="xapp-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
hint="在 https://api.slack.com/apps 中创建应用,查看OAuth & Permissions页面中的App-Level Token"
hint="Slack应用`OAuth & Permissions`页面中的`App-Level Token`"
persistent-hint
/>
</VCol>
<VCol cols="12" md="2">
@@ -325,7 +339,8 @@ onMounted(() => {
v-model="notificationSettings.SLACK_CHANNEL"
label="频道名称"
placeholder="全体"
hint="消息发送到的频道名称,不填写则发送到全体频道"
hint="消息发送频道,默认`全体`"
persistent-hint
/>
</VCol>
</VRow>
@@ -338,14 +353,16 @@ onMounted(() => {
<VTextField
v-model="notificationSettings.SYNOLOGYCHAT_WEBHOOK"
label="机器人传入URL"
hint="Synology Chat中创建机器人,获取机器人传入URL"
hint="Synology Chat机器人传入URL"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationSettings.SYNOLOGYCHAT_TOKEN"
label="令牌"
hint="Synology Chat中创建机器人,获取机器人令牌"
hint="Synology Chat机器人令牌"
persistent-hint
/>
</VCol>
</VRow>
@@ -355,13 +372,19 @@ onMounted(() => {
<VForm>
<VRow>
<VCol cols="12" md="4">
<VTextField v-model="notificationSettings.VOCECHAT_HOST" label="地址" />
<VTextField
v-model="notificationSettings.VOCECHAT_HOST"
label="地址"
hint="VoceChat服务端地址格式http(s)://ip:port"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.VOCECHAT_API_KEY"
label="机器人密钥"
hint="VoceChat中创建机器人,获取机器人密钥"
hint="VoceChat机器人密钥"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -369,7 +392,8 @@ onMounted(() => {
v-model="notificationSettings.VOCECHAT_CHANNEL_ID"
label="频道ID"
placeholder="不包含#号"
hint="VoceChat中创建频道,获取频道ID不包含#号"
hint="VoceChat频道ID不包含#号"
persistent-hint
/>
</VCol>
</VRow>

View File

@@ -294,7 +294,8 @@ onMounted(() => {
chips
:items="mediaSourcesDict"
label="当前使用数据源"
hint="选中多项时会同时展示来自不同数据源的搜索结果,选择的数据源顺序将会影响搜索结果的排序"
hint="搜索媒体信息时使用的数据源以及排序"
persistent-hint
/>
</VCol>
</VRow>
@@ -398,7 +399,8 @@ onMounted(() => {
v-model="defaultFilterRules.include"
type="text"
label="包含(关键字、正则式)"
hint="支持正式表达式,多个关键字用 | 分隔表示或"
hint="包含规则,支持正式表达式,多个关键字用 | 分隔表示或"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -406,7 +408,8 @@ onMounted(() => {
v-model="defaultFilterRules.exclude"
type="text"
label="排除(关键字、正则式)"
hint="支持正式表达式,多个关键字用 | 分隔表示或"
hint="排除规则,支持正式表达式,多个关键字用 | 分隔表示或"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -416,15 +419,17 @@ onMounted(() => {
label="最小做种数"
placeholder="0"
hint="小于该值的资源将被过滤掉0表示不过滤"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.min_seeders_time"
type="text"
label="最少做种数生效发布时间(分钟)"
label="最少做种数生效发布时间(分钟)"
placeholder="0"
hint="发布时间距现在大于该值的资源将生效最小做种数规则0表示不生效"
hint="发布时间距当前时间大于该值的资源将生效最小做种数规则0表示不生效"
persistent-hint
/>
</VCol>
</VRow>

View File

@@ -10,7 +10,7 @@ const $toast = useToast()
const schedulerList = ref<ScheduleInfo[]>([])
// 定时器
let refreshTimer: NodeJS.Timer | null = null
let refreshTimer: NodeJS.Timeout | null = null
// 调用API加载定时服务列表
async function loadSchedulerList() {

View File

@@ -149,7 +149,8 @@ onMounted(() => {
<VCheckbox
v-model="cookieCloudSetting.COOKIECLOUD_ENABLE_LOCAL"
label="启用本地CookieCloud服务器"
hint="启用后,将使用内建CookieCloud服务同步站点数据服务地址为http://localhost:3000/cookiecloud"
hint="使用内建CookieCloud服务同步站点数据服务地址为http://localhost:3000/cookiecloud"
persistent-hint
/>
</VCol>
</VRow>
@@ -157,17 +158,19 @@ onMounted(() => {
<VCol cols="12" md="6">
<VTextField
v-model="cookieCloudSetting.COOKIECLOUD_HOST"
label="远程CookieCloud服务地址"
label="服务地址"
placeholder="https://movie-pilot.org/cookiecloud"
:disabled="!!cookieCloudSetting.COOKIECLOUD_ENABLE_LOCAL"
hint="格式https://movie-pilot.org/cookiecloud"
hint="远端CookieCloud服务地址格式https://movie-pilot.org/cookiecloud"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="cookieCloudSetting.COOKIECLOUD_KEY"
label="用户KEY"
hint="CookieCloud浏览器插件生成"
hint="CookieCloud浏览器插件生成的用户KEY"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -175,7 +178,8 @@ onMounted(() => {
v-model="cookieCloudSetting.COOKIECLOUD_PASSWORD"
type="password"
label="端对端加密密码"
hint="CookieCloud浏览器插件生成"
hint="CookieCloud浏览器插件生成的端对端加密密码"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -183,14 +187,16 @@ onMounted(() => {
v-model="cookieCloudSetting.COOKIECLOUD_INTERVAL"
label="自动同步间隔"
:items="CookieCloudIntervalItems"
hint="设置定时从CookieCloud服务器同步站点Cookie到MoviePilot的时间周期"
hint="从CookieCloud服务器自动同步站点Cookie到MoviePilot的时间间隔"
persistent-hint
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="cookieCloudSetting.USER_AGENT"
label="浏览器User-Agent"
hint="设置为CookieCloud插件所在的浏览器的User-Agent,用于模拟浏览器请求,正确填写后有助于提升站点访问成功率"
hint="CookieCloud插件所在的浏览器的User-Agent"
persistent-hint
/>
</VCol>
</VRow>
@@ -215,7 +221,8 @@ onMounted(() => {
v-model="selectedTorrentPriority"
:items="TorrentPriorityItems"
label="当前使用下载优先规则"
hint="站点优先:优先下载站点优先级最高的站点的种子;做种数优先:优先下载做种数量最多的种子。注意下载优先级仍然低于搜索和订阅中设定的优先规则"
hint="同时命中多个站点的多个资源时下载的优先规则"
persistent-hint
/>
</VCol>
</VRow>
@@ -233,7 +240,8 @@ onMounted(() => {
<VCheckbox
v-model="isConfirmResetSites"
label="确认删除所有站点数据并重新同步。"
hint="删除所有站点数据并重新同步站点图标短时间内会因数缓存而混乱重启或者等待2两时自动恢复。"
hint="删除所有站点数据并重新从CookieCloud同步操作请先清空涉及站点的相关设置。"
persistent-hint
/>
</div>

View File

@@ -338,7 +338,8 @@ onMounted(() => {
v-model="selectedSubscribeMode"
:items="subscribeModeItems"
label="订阅模式"
hint="自动:系统自动爬取站点首页资源;站点RSS使用站点RSS订阅资源站点RSS会自动获取也可手动在站点管理中补全"
hint="自动:自动爬取站点首页站点RSS通过站点RSS链接订阅"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -346,7 +347,8 @@ onMounted(() => {
v-model="selectedRssInterval"
:items="rssIntervalItems"
label="站点RSS周期"
hint="设置站点RSS运行周期在订阅模式为站点RSS时生效"
hint="设置站点RSS运行周期在订阅模式为`站点RSS`时生效"
persistent-hint
/>
</VCol>
</VRow>
@@ -355,7 +357,8 @@ onMounted(() => {
<VSwitch
v-model="enableIntervalSearch"
label="开启订阅定时搜索"
hint="开启后系统每隔24小时将按名称搜索全站补全订阅可能漏掉的资源"
hint="每隔24小时全站搜索补全订阅可能漏掉的资源"
persistent-hint
/>
</VCol>
</VRow>
@@ -490,7 +493,8 @@ onMounted(() => {
v-model="defaultFilterRules.include"
type="text"
label="包含(关键字、正则式)"
hint="支持正式表达式,多个关键字用 | 分隔表示或"
hint="包含规则,支持正式表达式,多个关键字用 | 分隔表示或"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -498,7 +502,8 @@ onMounted(() => {
v-model="defaultFilterRules.exclude"
type="text"
label="排除(关键字、正则式)"
hint="支持正式表达式,多个关键字用 | 分隔表示或"
hint="排除规则,支持正式表达式,多个关键字用 | 分隔表示或"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -507,7 +512,8 @@ onMounted(() => {
type="text"
label="电影文件大小GB"
placeholder="0-30"
hint="格式0-30表示030GB之间的资源"
hint="文件大小范围,格式0-30表示0-30GB之间的资源"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -516,7 +522,8 @@ onMounted(() => {
type="text"
label="剧集单集文件大小GB"
placeholder="0-10"
hint="格式0-10表示010GB之间的资源"
hint="单集文件大小范围,格式0-10表示0-10GB之间的资源"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
@@ -526,15 +533,17 @@ onMounted(() => {
label="最小做种数"
placeholder="0"
hint="小于该值的资源将被过滤掉0表示不过滤"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.min_seeders_time"
type="text"
label="最少做种数生效发布时间(分钟)"
label="最少做种数生效发布时间(分钟)"
placeholder="0"
hint="发布时间距现在大于该值的资源将生效最小做种数规则0表示不生效"
hint="发布时间距当前时间大于该值的资源将生效最小做种数规则0表示不生效"
persistent-hint
/>
</VCol>
</VRow>

View File

@@ -244,14 +244,16 @@ onMounted(() => {
chips
:items="Downloaders"
label="当前使用下载器"
hint="MoviePilot自动添加的下载任务将使用选中的第1个下载器"
hint="启用下载器只有第1个会被默认下载使用"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="downloaderSettings.TORRENT_TAG"
label="下载器种子标签"
hint="设置种子标签用于区分MoviePilot添加的下载任务,默认标签为`MOVIEPILOT`"
hint="MoviePilot添加的下载任务标签"
persistent-hint
/>
</VCol>
</VRow>
@@ -259,8 +261,9 @@ onMounted(() => {
<VCol cols="12" md="6">
<VSwitch
v-model="downloaderSettings.DOWNLOADER_MONITOR"
label="监控默认下载器"
hint="监控选中的第1个下载器任务下载完成时自动整理文件到媒体库"
label="下载文件自动整理"
hint="任务下载完成时自动整理文件到媒体库"
persistent-hint
/>
</VCol>
</VRow>
@@ -278,8 +281,9 @@ onMounted(() => {
<VTextField
v-model="downloaderSettings.QB_HOST"
label="地址"
placeholder="IP:PORT"
hint="格式IP:PORT如启用了HTTPS请使用https://IP:PORT"
placeholder="http(s)://ip:port"
hint="服务端地址格式http(s)://ip:port"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -287,7 +291,8 @@ onMounted(() => {
v-model="downloaderSettings.QB_USER"
label="用户名"
placeholder="admin"
hint="QB的登录用户名"
hint="登录使用的用户名"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -295,28 +300,32 @@ onMounted(() => {
v-model="downloaderSettings.QB_PASSWORD"
type="password"
label="密码"
hint="QB的登录密码"
hint="登录使用的密码"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSwitch
v-model="downloaderSettings.QB_CATEGORY"
label="自动分类管理"
hint="开启后下载目录将由QB控制自动下载到分类到目录此时MoviePilot的下载目录设定无效需在QB中提前创建分类"
hint="由下载器自动管理分类和下载目录"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSwitch
v-model="downloaderSettings.QB_SEQUENTIAL"
label="顺序下载"
hint="开启后QB将按照文件顺序依次下载"
hint="顺序依次下载文件"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSwitch
v-model="downloaderSettings.QB_FORCE_RESUME"
label="强制继续"
hint="开启后QB将设置为强制继续、强制上传模式(带[F]标识)"
hint="强制继续、强制上传模式"
persistent-hint
/>
</VCol>
</VRow>
@@ -329,8 +338,9 @@ onMounted(() => {
<VTextField
v-model="downloaderSettings.TR_HOST"
label="地址"
placeholder="IP:PORT"
hint="格式IP:PORT如启用了HTTPS请使用https://IP:PORT"
placeholder="http(s)://ip:port"
hint="服务端地址格式http(s)://ip:port"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -338,7 +348,8 @@ onMounted(() => {
v-model="downloaderSettings.TR_USER"
label="用户名"
placeholder="admin"
hint="TR的登录用户名"
hint="登录使用的用户名"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -346,7 +357,8 @@ onMounted(() => {
v-model="downloaderSettings.TR_PASSWORD"
type="password"
label="密码"
hint="TR的登录密码"
hint="登录使用的密码"
persistent-hint
/>
</VCol>
</VRow>
@@ -384,7 +396,8 @@ onMounted(() => {
chips
:items="MediaServers"
label="当前使用媒体服务器"
hint="媒体服务器用于搜索下载等判断库中是否已存在,以避免重复下载"
hint="启用媒体服务器,入库展示、下载控重等将使用"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -392,7 +405,8 @@ onMounted(() => {
v-model="mediaServerSettings.MEDIASERVER_SYNC_INTERVAL"
:items="syncIntervalItems"
label="同步周期"
hint="设置后数据将定时同步到MoviePilot数据库,以便展示媒体库是否存在标识"
hint="同步媒体库数据到MoviePilot的时间间隔"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -400,7 +414,8 @@ onMounted(() => {
v-model="mediaServerSettings.MEDIASERVER_SYNC_BLACKLIST"
label="媒体库同步黑名单"
placeholder="使用,分隔"
hint="设置不同步数据的媒体库名称,使用,分隔,如:电影,电视剧"
hint="不同步数据的媒体库名称,多个使用,分隔"
persistent-hint
/>
</VCol>
</VRow>
@@ -419,8 +434,9 @@ onMounted(() => {
<VTextField
v-model="mediaServerSettings.EMBY_HOST"
label="地址"
placeholder="IP:PORT"
hint="格式:IP:PORT 或 http(s)://IP:PORT/"
placeholder="http(s)://ip:port"
hint="服务端地址,格式http(s)://ip:port"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -428,14 +444,16 @@ onMounted(() => {
v-model="mediaServerSettings.EMBY_PLAY_HOST"
label="外网播放地址"
placeholder="http(s)://domain:port"
hint="格式http(s)://domain:port设置后跳转Emby时将优先使用此地址"
hint="跳转播放页面使用的地址,格式http(s)://domain:port"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="mediaServerSettings.EMBY_API_KEY"
label="API密钥"
hint="Emby的API密钥Emby设置->高级->API 密钥 中生成"
hint="Emby设置->高级->API密钥中生成的密钥"
persistent-hint
/>
</VCol>
</VRow>
@@ -448,8 +466,9 @@ onMounted(() => {
<VTextField
v-model="mediaServerSettings.JELLYFIN_HOST"
label="地址"
placeholder="IP:PORT"
hint="格式:IP:PORT 或 http(s)://IP:PORT/"
placeholder="http(s)://ip:port"
hint="服务端地址,格式http(s)://ip:port"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -457,14 +476,16 @@ onMounted(() => {
v-model="mediaServerSettings.JELLYFIN_PLAY_HOST"
label="外网播放地址"
placeholder="http(s)://domain:port"
hint="格式http(s)://domain:port设置后跳转Jellyfin时将优先使用此地址"
hint="跳转播放页面使用的地址,格式http(s)://domain:port"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="mediaServerSettings.JELLYFIN_API_KEY"
label="API密钥"
hint="Jellyfin的API密钥Jellyfin设置->高级->API 密钥 中生成"
hint="Jellyfin设置->高级->API密钥中生成的密钥"
persistent-hint
/>
</VCol>
</VRow>
@@ -477,8 +498,9 @@ onMounted(() => {
<VTextField
v-model="mediaServerSettings.PLEX_HOST"
label="地址"
placeholder="IP:PORT"
hint="格式:IP:PORT 或 http(s)://IP:PORT/"
placeholder="http(s)://ip:port"
hint="服务端地址,格式http(s)://ip:port"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
@@ -486,14 +508,16 @@ onMounted(() => {
v-model="mediaServerSettings.PLEX_PLAY_HOST"
label="外网播放地址"
placeholder="http(s)://domain:port"
hint="格式http(s)://domain:port设置后跳转Plex时将优先使用此地址"
hint="跳转播放页面使用的地址,格式http(s)://domain:port"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="mediaServerSettings.PLEX_TOKEN"
label="API密钥"
hint="Plex网页Url中的X-Plex-Token通过浏览器F12->网络请求URL中获取"
label="X-Plex-Token"
hint="浏览器F12->网络从Plex请求URL中获取的X-Plex-Token"
persistent-hint
/>
</VCol>
</VRow>

View File

@@ -21,8 +21,7 @@ const transferExcludeWords = ref('')
async function queryCustomIdentifiers() {
try {
const result: { [key: string]: any } = await api.get('system/setting/CustomIdentifiers')
customIdentifiers.value = result.data?.value.join('\n')
if (result && result.data && result.data.value) customIdentifiers.value = result.data.value.join('\n')
} catch (error) {
console.log(error)
}
@@ -32,8 +31,7 @@ async function queryCustomIdentifiers() {
async function queryCustomReleaseGroups() {
try {
const result: { [key: string]: any } = await api.get('system/setting/CustomReleaseGroups')
customReleaseGroups.value = result.data?.value.join('\n')
if (result && result.data && result.data.value) customReleaseGroups.value = result.data.value.join('\n')
} catch (error) {
console.log(error)
}
@@ -43,8 +41,7 @@ async function queryCustomReleaseGroups() {
async function queryCustomization() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Customization')
customization.value = result.data?.value.join('\n')
if (result && result.data && result.data.value) customization.value = result.data?.value.join('\n')
} catch (error) {
console.log(error)
}
@@ -54,8 +51,7 @@ async function queryCustomization() {
async function queryTransferExcludeWords() {
try {
const result: { [key: string]: any } = await api.get('system/setting/TransferExcludeWords')
transferExcludeWords.value = result.data?.value.join('\n')
if (result && result.data && result.data.value) transferExcludeWords.value = result.data?.value.join('\n')
} catch (error) {
console.log(error)
}
@@ -147,6 +143,7 @@ onMounted(() => {
auto-grow
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组"
hint="支持正则表达式,特殊字符需要\转义,一行为一组"
persistent-hint
/>
</VCardText>
<VCardText>
@@ -181,6 +178,7 @@ onMounted(() => {
auto-grow
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个制作组/字幕组"
hint="支持正则表达式,特殊字符需要\转义,一行代表一个制作组/字幕组"
persistent-hint
/>
</VCardText>
<VCardText>
@@ -198,8 +196,9 @@ onMounted(() => {
<VTextarea
v-model="customization"
auto-grow
placeholder="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义"
hint="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义"
placeholder="支持正则表达式,特殊字符需要\转义,多个匹配对象请换行分隔"
hint="支持正则表达式,特殊字符需要\转义,多个匹配对象请换行分隔"
persistent-hint
/>
</VCardText>
<VCardText>
@@ -219,6 +218,7 @@ onMounted(() => {
auto-grow
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词"
hint="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词"
persistent-hint
/>
</VCardText>
<VCardText>

View File

@@ -67,7 +67,7 @@ onBeforeMount(fetchData)
error-description="已添加并支持的站点将会在这里显示"
/>
<!-- 新增站点按钮 -->
<VFab icon="mdi-plus" location="bottom end" size="x-large" fixed app appear @click="siteAddDialog = true" />
<VFab icon="mdi-plus" location="bottom" size="x-large" fixed app appear @click="siteAddDialog = true" />
<!-- 新增站点弹窗 -->
<SiteAddEditDialog
v-if="siteAddDialog"

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import PullRefresh from 'pull-refresh-vue3'
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'
import api from '@/api'
import type { Subscribe } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
@@ -11,6 +11,7 @@ import store from '@/store'
// 输入参数
const props = defineProps({
type: String,
subid: String,
})
// 是否刷新过
@@ -35,9 +36,6 @@ async function fetchData() {
}
}
// 加载时获取数据
onBeforeMount(fetchData)
// 刷新状态
const loading = ref(false)
@@ -56,11 +54,23 @@ const filteredDataList = computed(() => {
if (superUser) return dataList.value.filter(data => data.type === props.type)
else return dataList.value.filter(data => data.type === props.type && data.username === userName)
})
onMounted(async () => {
await fetchData()
if (props.subid) {
// 找到这个订阅
const sub = dataList.value.find(sub => sub.id.toString() == props.subid?.toString())
if (sub) {
// 打开编辑弹窗
sub.page_open = true
}
}
})
</script>
<template>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<PullRefresh v-model="loading" @refresh="onRefresh">
<VPullToRefresh v-model="loading" @load="onRefresh">
<div v-if="filteredDataList.length > 0" class="mx-3 grid gap-4 grid-subscribe-card p-1">
<SubscribeCard
v-for="data in filteredDataList"
@@ -76,12 +86,12 @@ const filteredDataList = computed(() => {
error-title="没有订阅"
error-description="请通过搜索添加电影电视剧订阅"
/>
</PullRefresh>
</VPullToRefresh>
<!-- 底部操作按钮 -->
<VFab
v-if="store.state.auth.superUser"
icon="mdi-clipboard-edit"
location="bottom end"
location="bottom"
size="x-large"
fixed
app
@@ -92,8 +102,8 @@ const filteredDataList = computed(() => {
v-if="store.state.auth.superUser"
icon="mdi-history"
color="info"
location="bottom end"
class="mb-2"
location="bottom"
class="mb-16"
size="x-large"
fixed
app

View File

@@ -0,0 +1,478 @@
<script setup lang="ts">
import api from '@/api'
import type { Plugin, Subscribe } from '@/api/types'
import {
SystemNavMenus,
UserfulMenus,
SubscribeMovieTabs,
SubscribeTvTabs,
PluginTabs,
SettingTabs,
} from '@/router/menu'
import { NavMenu } from '@/@layouts/types'
// 路由
const router = useRouter()
// 定义事件
const emit = defineEmits(['close'])
// 搜索词
const searchWord = ref<string | null>(null)
// ref
const searchWordInput = ref<HTMLElement | null>(null)
// 近期搜索词条
const recentSearches = ref<string[]>([])
// 保存近期搜索到本地
function saveRecentSearches(keyword: string) {
if (!keyword) return
if (recentSearches.value.includes(keyword)) return
recentSearches.value.unshift(keyword)
localStorage.setItem('MP_RecentSearches', JSON.stringify(recentSearches.value))
}
// 从本地加载近期搜索
function loadRecentSearches() {
const recentSearchesStr = localStorage.getItem('MP_RecentSearches')
if (recentSearchesStr) {
recentSearches.value = JSON.parse(recentSearchesStr)
// 只保留最近的 5 条
if (recentSearches.value.length > 5) {
recentSearches.value = recentSearches.value.slice(0, 5)
}
}
}
// 所有菜单功能
function getMenus(): NavMenu[] {
let menus: NavMenu[] = []
// 导航菜单
SystemNavMenus.forEach(
item =>
item &&
menus.push({
title: item.title,
icon: item.icon,
to: item.to,
header: item.header,
admin: item.admin,
}),
)
// 各类标签页
SettingTabs.forEach(
item =>
item &&
menus.push({
title: '设定 -> ' + item.title,
icon: item.icon,
to: `/setting?tab=${item.tab}`,
header: '',
admin: true,
description: item.description,
}),
)
SubscribeMovieTabs.forEach(
item =>
item &&
menus.push({
title: '电影 -> ' + item.title,
icon: item.icon,
to: `/subscribe-movie?tab=${item.tab}`,
header: '',
admin: false,
}),
)
SubscribeTvTabs.forEach(
item =>
item &&
menus.push({
title: '电视剧 -> ' + item.title,
icon: item.icon,
to: `/subscribe-tv?tab=${item.tab}`,
header: '',
admin: false,
}),
)
PluginTabs.forEach(
item =>
item &&
menus.push({
title: '插件 -> ' + item.title,
icon: item.icon,
to: `/plugins?tab=${item.tab}`,
header: '',
admin: true,
}),
)
return menus
}
// 匹配的菜单列表
const matchedMenuItems = computed(() => {
if (!searchWord.value) return []
const lowerWord = (searchWord.value as string).toLowerCase()
const menuItems = getMenus()
if (menuItems)
return menuItems.filter(
item =>
item.title.toLowerCase().includes(lowerWord) ||
(item.description && item.description.toLowerCase().includes(lowerWord)),
)
return []
})
// 所有插件(已安装)
const pluginItems = ref<Plugin[]>([])
// 获取插件列表数据
async function fetchInstalledPlugins() {
try {
pluginItems.value = await api.get('plugin/', {
params: {
state: 'installed',
},
})
} catch (error) {
console.error(error)
}
}
// 区配的插件列表
const matchedPluginItems = computed(() => {
if (!searchWord.value) return []
const lowerWord = (searchWord.value as string).toLowerCase()
return pluginItems.value.filter((item: Plugin) => {
if (!item.plugin_name && !item.plugin_desc) return false
return item.plugin_name?.toLowerCase().includes(lowerWord) || item.plugin_desc?.toLowerCase().includes(lowerWord)
})
})
// 所有订阅数据
const SubscribeItems = ref<Subscribe[]>([])
// 获取电影订阅列表数据
async function fetchSubscribes() {
try {
SubscribeItems.value = await api.get('subscribe/')
} catch (error) {
console.error(error)
}
}
// 匹配的订阅列表
const matchedSubscribeItems = computed(() => {
if (!searchWord.value) return []
const lowerWord = (searchWord.value as string).toLowerCase()
return SubscribeItems.value.filter((item: Subscribe) => {
return item.name.toLowerCase().includes(lowerWord)
})
})
// 跳转媒体搜索页面
function searchMedia(searchType: string) {
// 搜索类型 media/person
if (!searchWord.value) return
saveRecentSearches(searchWord.value)
router.push({
path: '/browse/media/search',
query: {
title: searchWord.value,
type: searchType,
},
})
emit('close')
}
// 跳转到种子搜索页面
function searchTorrent() {
if (!searchWord.value) return
saveRecentSearches(searchWord.value)
router.push({
path: '/resource',
query: {
keyword: searchWord.value,
area: 'title',
},
})
emit('close')
}
// 跳转到历史记录页面
function searchHistory() {
if (!searchWord.value) return
saveRecentSearches(searchWord.value)
router.push({
path: '/history',
query: {
search: searchWord.value,
},
})
emit('close')
}
// 跳转插件页面
function showPlugin(pluginId: string) {
router.push({
path: `/plugins/`,
query: {
tab: 'installed',
id: pluginId,
},
})
emit('close')
}
// 跳转菜单页面
function goPage(to: string) {
router.push(to)
emit('close')
}
// 跳转订阅页面
function goSubscribe(subscribe: Subscribe) {
if (subscribe.type === '电影') {
router.push({
path: '/subscribe-movie',
query: {
id: subscribe.id,
},
})
} else {
router.push({
path: '/subscribe-tv',
query: {
id: subscribe.id,
},
})
}
emit('close')
}
onMounted(() => {
setTimeout(() => {
searchWordInput.value?.focus()
}, 500)
fetchInstalledPlugins()
fetchSubscribes()
loadRecentSearches()
})
</script>
<template>
<VDialog max-width="40rem" scrollable>
<VCard>
<VCardItem class="pe-12">
<VCombobox
ref="searchWordInput"
v-model="searchWord"
density="compact"
variant="plain"
class="text-high-emphasis"
placeholder="搜索 ..."
@keydown.enter="searchMedia('media')"
>
<template #prepend>
<VIcon icon="ri-search-line" style="opacity: 1" />
</template>
</VCombobox>
</VCardItem>
<DialogCloseBtn inner-class="absolute right-3 top-5 text-high-emphasis" @click="emit('close')" />
<VDivider />
<VCardText class="p-0">
<VList lines="one" v-if="searchWord">
<!-- 搜索结果 -->
<VListSubheader v-if="searchWord"> 媒体 & 资源 </VListSubheader>
<VHover>
<template #default="hover">
<VListItem
prepend-icon="mdi-movie-search"
density="compact"
link
v-bind="hover.props"
@click="searchMedia('media')"
>
<VListItemTitle>
搜索 <span class="font-bold">{{ searchWord }} </span> 相关的电影电视剧 ...
</VListItemTitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
<VHover>
<template #default="hover">
<VListItem prepend-icon="mdi-account-search" link v-bind="hover.props" @click="searchMedia('person')">
<VListItemTitle>
搜索 <span class="font-bold">{{ searchWord }}</span> 相关的演职人员 ...
</VListItemTitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
<VHover>
<template #default="hover">
<VListItem prepend-icon="mdi-search-web" link v-bind="hover.props" @click="searchTorrent">
<VListItemTitle>
搜索 <span class="font-bold">{{ searchWord }}</span> 相关的站点资源 ...
</VListItemTitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
<VHover>
<template #default="hover">
<VListItem prepend-icon="mdi-history" link v-bind="hover.props" @click="searchHistory">
<VListItemTitle>
搜索 <span class="font-bold">{{ searchWord }}</span> 相关的历史记录 ...
</VListItemTitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
<VListSubheader v-if="matchedSubscribeItems.length > 0"> 订阅 </VListSubheader>
<VHover
v-if="matchedSubscribeItems.length > 0"
v-for="subscribe in matchedSubscribeItems"
:key="subscribe.id"
>
<template #default="hover">
<VListItem
:prepend-icon="`${subscribe.type === '电影' ? 'mdi-movie-roll' : 'mdi-television-classic'}`"
density="compact"
link
v-bind="hover.props"
@click="goSubscribe(subscribe)"
>
<VListItemTitle>
{{ subscribe.name }}<span v-if="subscribe.season"> {{ subscribe.season }} </span>
</VListItemTitle>
<VListItemSubtitle> {{ subscribe.type }}</VListItemSubtitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
<VListSubheader v-if="matchedMenuItems.length > 0"> 功能 </VListSubheader>
<VHover v-if="matchedMenuItems.length > 0" v-for="menu in matchedMenuItems" :key="menu.title">
<template #default="hover">
<VListItem
:prepend-icon="menu.icon as string"
density="compact"
link
v-bind="hover.props"
@click="goPage(menu.to as string)"
>
<VListItemTitle>
{{ menu.title }}
</VListItemTitle>
<VListItemSubtitle v-if="menu.description"> {{ menu.description }} </VListItemSubtitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
<VListSubheader v-if="matchedPluginItems.length > 0"> 插件 </VListSubheader>
<VHover v-if="matchedPluginItems.length > 0" v-for="plugin in matchedPluginItems" :key="plugin.id">
<template #default="hover">
<VListItem
prepend-icon="mdi-puzzle"
density="compact"
link
v-bind="hover.props"
@click="showPlugin(plugin.id ?? '')"
>
<VListItemTitle> {{ plugin.plugin_name }} </VListItemTitle>
<VListItemSubtitle> {{ plugin.plugin_desc }} </VListItemSubtitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
</VList>
<div v-else>
<!-- 默认 -->
<VCardText>
<VRow v-if="recentSearches.length > 0">
<VCol cols="12">
<p class="custom-letter-spacing text-sm text-disabled text-uppercase py-2 px-4 mb-0">最近搜索</p>
<div class="px-3">
<VChip
v-for="(word, index) in recentSearches"
:key="index"
class="me-2"
variant="tonal"
@click="searchWord = word"
label
>
{{ word }}
</VChip>
</div>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<p class="custom-letter-spacing text-sm text-disabled text-uppercase py-2 px-4 mb-0">常用功能</p>
<VList lines="one">
<VHover v-for="(menu, index) in UserfulMenus" :key="index">
<template #default="hover">
<VListItem
:prepend-icon="menu.icon"
density="compact"
link
v-bind="hover.props"
@click="goPage(menu.to)"
>
<VListItemTitle>
{{ menu.title }}
</VListItemTitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
</VList>
</VCol>
<VCol cols="12" md="6">
<p class="custom-letter-spacing text-sm text-disabled text-uppercase py-2 px-4 mb-0">常用插件</p>
<VList lines="one">
<VHover v-for="plugin in pluginItems.slice(0, 5)" :key="plugin.id">
<template #default="hover">
<VListItem
prepend-icon="mdi-puzzle"
density="compact"
link
v-bind="hover.props"
@click="showPlugin(plugin.id ?? '')"
>
<VListItemTitle> {{ plugin.plugin_name }} </VListItemTitle>
<template #append>
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
</template>
</VListItem>
</template>
</VHover>
</VList>
</VCol>
<VCol cols="12" md="6"> </VCol>
<VCol cols="12" md="6"> </VCol>
</VRow>
</VCardText>
</div>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -46,7 +46,8 @@
"esnext",
"dom",
"dom.iterable",
"scripthost"
"scripthost",
"WebWorker"
],
"skipLibCheck": true,
"types": [
@@ -63,11 +64,13 @@
"src/**/*.vue",
"themeConfig.ts",
"auto-imports.d.ts",
"components.d.ts"
"components.d.ts",
"src/service-worker.ts",
"public/service.js"
],
"exclude": [
"dist",
"node_modules",
"src/@iconify/*"
]
}
}

View File

@@ -4,8 +4,8 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
import vuetify from 'vite-plugin-vuetify'
import { VitePWA } from 'vite-plugin-pwa'
// https://vitejs.dev/config/
export default defineConfig({
@@ -13,8 +13,6 @@ export default defineConfig({
plugins: [
vue(),
vueJsx(),
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
vuetify({
styles: {
configFile: 'src/styles/variables/_vuetify.scss',
@@ -29,12 +27,100 @@ export default defineConfig({
vueTemplate: true,
}),
VitePWA({
registerType: 'autoUpdate',
injectRegister: 'script',
manifest: false,
registerType: 'autoUpdate',
strategies: 'injectManifest',
srcDir: 'src',
filename: 'service-worker.ts',
workbox: {
navigateFallbackDenylist: [
/.*\/api\/v\d+\/system\/logging.*/,
globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg,jpeg}'],
navigateFallbackDenylist: [/.*\/api\/v\d+\/system\/logging.*/],
},
injectManifest: {
rollupFormat: 'iife',
},
devOptions: {
enabled: true,
type: 'module',
},
manifest: {
'name': 'MoviePilot',
'short_name': 'MoviePilot',
'start_url': './',
'display': 'standalone',
'icons': [
{
'src': './android-chrome-192x192.png',
'sizes': '192x192',
'type': 'image/png',
'purpose': 'any',
},
{
'src': './android-chrome-192x192_maskable.png',
'sizes': '192x192',
'type': 'image/png',
'purpose': 'maskable',
},
{
'src': './android-chrome-512x512.png',
'sizes': '512x512',
'type': 'image/png',
'purpose': 'any',
},
{
'src': './android-chrome-512x512_maskable.png',
'sizes': '512x512',
'type': 'image/png',
'purpose': 'maskable',
},
],
'theme_color': '#28243D',
'background_color': '#28243D',
'shortcuts': [
{
'name': '推荐',
'url': './ranking',
'icons': [
{
'src': './sparkles-icon-192x192.png',
'sizes': '192x192',
'type': 'image/png',
},
],
},
{
'name': '电影订阅',
'url': './subscribe-movie?tab=mysub',
'icons': [
{
'src': './clock-icon-192x192.png',
'sizes': '192x192',
'type': 'image/png',
},
],
},
{
'name': '电视剧订阅',
'url': './subscribe-tv?tab=mysub',
'icons': [
{
'src': './clock-icon-192x192.png',
'sizes': '192x192',
'type': 'image/png',
},
],
},
{
'name': '设置',
'url': './setting',
'icons': [
{
'src': './cog-icon-192x192.png',
'sizes': '192x192',
'type': 'image/png',
},
],
},
],
},
}),
@@ -63,8 +149,6 @@ export default defineConfig({
},
optimizeDeps: {
exclude: ['vuetify'],
entries: [
'./src/**/*.vue',
],
entries: ['./src/**/*.vue'],
},
})

1436
yarn.lock

File diff suppressed because it is too large Load Diff