Compare commits

..

86 Commits

Author SHA1 Message Date
jxxghp
5a3673efc6 更新 package.json 2023-10-14 14:34:16 +08:00
jxxghp
c03ec1d741 fix ui 2023-10-14 14:29:57 +08:00
jxxghp
e62d0809b3 fix ui 2023-10-14 14:15:38 +08:00
jxxghp
7f13597517 feat merge form 2023-10-14 13:48:02 +08:00
jxxghp
c822f1fffd feat 整合站点编辑组件 2023-10-14 09:13:38 +08:00
jxxghp
14ca74a29d Merge branch 'main' of https://github.com/jxxghp/MoviePilot-Frontend 2023-10-13 22:56:07 +08:00
jxxghp
3ee897a350 fix 2023-10-13 22:56:02 +08:00
jxxghp
789aac60c9 更新 package.json 2023-10-13 22:39:49 +08:00
jxxghp
2c73a8f3e1 fix ui 2023-10-13 22:37:04 +08:00
jxxghp
539bc656f8 fix 2023-10-13 22:33:58 +08:00
jxxghp
feda0cad2d feat 默认过滤规则拆分 2023-10-13 21:28:34 +08:00
jxxghp
c723d89739 fix 2023-10-13 17:30:14 +08:00
jxxghp
0a0e7a059a fix 2023-10-13 17:29:23 +08:00
jxxghp
0263fbbee6 fix 2023-10-13 17:26:12 +08:00
jxxghp
e205296e22 feat 订阅实时编辑 2023-10-13 17:24:18 +08:00
jxxghp
261f5a9c68 fix #822 2023-10-13 15:14:26 +08:00
jxxghp
fa097651f4 fix rules 2023-10-13 11:41:27 +08:00
jxxghp
c94d5f7e7d fix bug 2023-10-12 22:45:59 +08:00
jxxghp
e34f18799f fix 2023-10-12 22:25:15 +08:00
jxxghp
1681a311f7 fix 2023-10-12 21:44:48 +08:00
jxxghp
da08d8ec19 fix ui 2023-10-12 21:36:08 +08:00
jxxghp
730178c838 fix 2023-10-12 20:02:24 +08:00
jxxghp
a04450ae98 feat 60fps 2023-10-12 17:06:37 +08:00
jxxghp
2b2fd66a29 fix bug 2023-10-12 16:31:17 +08:00
jxxghp
58fe08ad3d build ui 2023-10-12 16:30:27 +08:00
jxxghp
240d6bede0 fix ui 2023-10-12 16:26:05 +08:00
jxxghp
23d808f8b1 feat 资源页面视图切换 2023-10-12 16:09:15 +08:00
jxxghp
2f293706cb fix 2023-10-12 09:46:04 +08:00
jxxghp
9aaaf0c520 feat 更多订阅设置项 2023-10-12 09:43:55 +08:00
jxxghp
6694e7e929 feat 搜索框聚焦、发现页缓存 2023-10-12 08:13:05 +08:00
jxxghp
d3768cb994 feat switch view button 2023-10-11 21:30:59 +08:00
jxxghp
c59d3e28b9 feat torrents page 2023-10-11 21:21:49 +08:00
jxxghp
914239f434 feat 热门动漫 2023-10-11 16:12:38 +08:00
jxxghp
7a5d04dc53 add rclone 2023-10-11 11:47:45 +08:00
jxxghp
110fe39e72 Merge pull request #50 from thsrite/main
fix customization
2023-10-10 13:57:14 +08:00
thsrite
9689a86151 fix customization 2023-10-10 13:54:57 +08:00
jxxghp
6462ae5956 fix 2023-10-10 09:15:01 +08:00
jxxghp
053963d050 nginx conf 2023-10-10 08:36:09 +08:00
jxxghp
8a95549118 remove win builder 2023-10-10 08:09:29 +08:00
jxxghp
46e8fa551c fix plugin uri 2023-10-10 08:06:34 +08:00
jxxghp
be2034d75b fix build 2023-10-09 21:28:39 +08:00
jxxghp
634fa58048 fix build 2023-10-09 19:21:51 +08:00
jxxghp
cd5c093557 fix build 2023-10-09 19:16:14 +08:00
jxxghp
76cf86385e add pkg 2023-10-09 19:13:31 +08:00
jxxghp
5c5ed5d7ee add express 2023-10-09 18:40:55 +08:00
jxxghp
47e7a37667 Merge pull request #48 from thsrite/customization 2023-10-08 12:22:40 +08:00
thsrite
d642ab42be feat 自定义占位符 2023-10-08 12:13:21 +08:00
jxxghp
b4de1c99d5 repack 2023-10-08 09:19:23 +08:00
jxxghp
53e35eb9ff fix ui 2023-10-08 09:05:37 +08:00
jxxghp
b222098ec5 Update package.json 2023-10-07 05:55:15 -07:00
jxxghp
bb8cf7ed78 Merge pull request #47 from thsrite/main
feat 药丸论坛签到
2023-10-07 20:54:37 +08:00
thsrite
0219ce3a9c Merge remote-tracking branch 'origin/main' into main 2023-10-07 20:50:07 +08:00
thsrite
b82e5d7cba feat 药丸论坛签到 2023-10-07 20:49:59 +08:00
jxxghp
ccee71e638 fix ui 2023-09-29 08:55:33 +08:00
jxxghp
cba0e739eb 更新 package.json 2023-09-28 17:55:17 +08:00
jxxghp
c569cb9cde add icon 2023-09-28 17:11:04 +08:00
jxxghp
fc585a3900 v1.2.7 2023-09-28 16:16:04 +08:00
jxxghp
973f8529c2 Merge pull request #46 from thsrite/main
feat 自动清理媒体库插件icon
2023-09-28 15:46:36 +08:00
thsrite
1ff9dc50fd feat 自动清理媒体库插件icon 2023-09-28 15:12:15 +08:00
jxxghp
065c9053da fix ui 2023-09-28 12:52:44 +08:00
jxxghp
6905be1bcd fix #681 2023-09-28 09:58:39 +08:00
jxxghp
a550f9616c feat 批量整理进度条 2023-09-27 14:26:59 +08:00
jxxghp
bcee3e5373 v1.2.6 2023-09-27 10:19:05 +08:00
jxxghp
d377ced6b6 feat 优先级规则支持动态调整 2023-09-27 09:42:28 +08:00
jxxghp
6e0ceb093c feat 批量重新整理 2023-09-27 08:59:09 +08:00
jxxghp
745f99e52e feat 历史记录批量重新整理 2023-09-27 08:45:55 +08:00
jxxghp
7197034eda fix ui 2023-09-24 19:52:55 +08:00
jxxghp
264748652f fix 2023-09-24 19:47:28 +08:00
jxxghp
48e214564a v1.2.5 2023-09-24 19:30:37 +08:00
jxxghp
5424e7e02a fix ui 2023-09-24 12:26:42 +08:00
jxxghp
0c9c70b067 feat 服务设置 2023-09-24 11:14:27 +08:00
jxxghp
0ff24f4b09 fix torrent ui 2023-09-23 11:55:49 +08:00
jxxghp
cfa75b7643 rename 2023-09-23 08:32:24 +08:00
jxxghp
b72ad1d78d Merge pull request #45 from jianxcao/feature-url-307
feat: 修改url地址,防止307成错误的url地址
2023-09-22 15:34:14 +08:00
jxxghp
5d1f293606 feat SynologyChat 2023-09-22 15:32:51 +08:00
jianxiong.cao
2dc0eca4aa feat: 修改url地址,防止307成错误的url地址 2023-09-22 15:11:52 +08:00
jxxghp
f5808c1c81 fix 光标聚焦 2023-09-22 13:49:00 +08:00
jxxghp
321037477f fix placeholder 2023-09-22 11:03:58 +08:00
jxxghp
43589c66e9 fix bug 2023-09-22 09:20:41 +08:00
jxxghp
435f299a8b fix ui 2023-09-22 07:27:27 +08:00
jxxghp
083db80251 更新 MediaCard.vue 2023-09-21 23:21:04 +08:00
jxxghp
92bf520cf4 fix 2023-09-21 23:06:51 +08:00
jxxghp
ab354f21c4 fix ui 2023-09-21 22:57:54 +08:00
jxxghp
c7a2c045c7 fix ui 2023-09-21 22:50:35 +08:00
jxxghp
d33c8942e4 fix ui 2023-09-21 22:28:18 +08:00
jxxghp
5e630097b9 更新 MediaCard.vue 2023-09-21 21:52:56 +08:00
91 changed files with 3084 additions and 1449 deletions

View File

@@ -1,4 +1,4 @@
name: Build moviepilot frontend
name: Build Moviepilot-Frontend
on:
workflow_dispatch:

View File

@@ -1,7 +1,8 @@
{
"name": "moviepilot",
"version": "1.2.4",
"version": "1.3.3-1",
"private": true,
"bin": "dist/service.js",
"scripts": {
"dev": "vite --host",
"build": "vite build",
@@ -9,7 +10,13 @@
"typecheck": "vue-tsc --noEmit",
"lint": "eslint . -c .eslintrc.js --fix --ext .ts,.js,.vue,.tsx,.jsx",
"build:icons": "tsc -b src/@iconify && node src/@iconify/build-icons.js",
"postinstall": "npm run build:icons"
"postinstall": "npm run build:icons",
"pkg": "pkg . -t node18-win-x64 -o MoviePilot-Frontend.exe"
},
"pkg": {
"assets": [
"dist/**/*"
]
},
"dependencies": {
"@casl/ability": "^6.2.0",
@@ -21,6 +28,8 @@
"axios": "1.4.0",
"axios-mock-adapter": "^1.21.4",
"chart.js": "^4.1.2",
"express": "^4.18.2",
"express-http-proxy": "^2.0.0",
"jwt-decode": "^3.1.2",
"nprogress": "^0.2.0",
"postcss-purgecss": "^5.0.0",

86
public/nginx.conf Normal file
View File

@@ -0,0 +1,86 @@
worker_processes auto;
events {
worker_connections 1024;
}
http {
sendfile on;
keepalive_timeout 3600;
server {
include mime.types;
default_type application/octet-stream;
listen 3000;
listen [::]:3000;
server_name moviepilot;
location / {
# 主目录
expires off;
add_header Cache-Control "no-cache, no-store, must-revalidate";
root html;
try_files $uri $uri/ /index.html;
}
location /assets {
# 静态资源
expires 7d;
add_header Cache-Control "public";
root html;
}
location ~ ^/api/v1/system/(message|progress/) {
# SSE MIME类型设置
default_type text/event-stream;
# 禁用缓存
add_header Cache-Control no-cache;
add_header X-Accel-Buffering no;
proxy_buffering off;
proxy_cache off;
# 代理设置
proxy_pass http://backend_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 超时设置
proxy_read_timeout 3600s;
}
location /api {
# 后端API
proxy_pass http://backend_api;
rewrite ^.+mock-server/?(.*)$ /$1 break;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_redirect off;
proxy_set_header Connection "";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
# 超时设置
proxy_read_timeout 600s;
}
}
upstream backend_api {
# 后端API的地址和端口
server 127.0.0.1:3001;
# 可以添加更多后端服务器作为负载均衡
}
}

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

View File

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

36
public/service.js Normal file
View File

@@ -0,0 +1,36 @@
const path = require('node:path')
const express = require('express')
const proxy = require('express-http-proxy')
const app = express()
const port = process.env.NGINX_PORT || 3000
// 后端 API 地址
const proxyConfig = {
URL: '127.0.0.1',
PORT: process.env.PORT || 3001
}
// 静态文件服务目录
app.use(express.static(__dirname))
// 配置代理中间件将请求转发给后端API
app.use(
'/api',
proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
// 路径加上 /api 前缀
proxyReqPathResolver: (req) => {
return `/api${req.url}`
}
})
);
// 处理根路径的请求
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html')) // 指向你的前端入口文件
})
app.listen(port, () => {
console.log(`Server is running on port ${port}`)
})

View File

@@ -44,6 +44,15 @@ export interface Subscribe {
// 排除
exclude?: string
// 质量
quality?: string
// 分辨率
resolution?: string
// 特效
effect?: string
// 总集数
total_episode?: number
@@ -68,8 +77,8 @@ export interface Subscribe {
// 订阅站点
sites: number[]
// 是否洗版
best_version: number
// 是否洗版数字或者boolean
best_version: any
// 当前优先级
current_priority: number
@@ -87,7 +96,7 @@ export interface TransferHistory {
// 目的目录
dest?: string
// 转移模式link/copy/move/softlink
// 转移模式link/copy/move/softlink/rclone_copy/rclone_move
mode?: string
// 类型:电影、电视剧
@@ -407,13 +416,13 @@ export interface Site {
ua?: string
// 是否使用代理
proxy?: number
proxy?: any
// 过滤规则
filter?: string
// 是否演染
render?: number
render?: any
// 是否公开站点
public?: number
@@ -832,6 +841,7 @@ export interface NotificationSwitch {
wechat: boolean
telegram: boolean
slack: boolean
synologychat: boolean
}
// 环境设置

View File

@@ -2,19 +2,30 @@
// 输入参数
const props = defineProps({
pri: String,
maxpri: String,
rules: Array as PropType<string[]>,
width: String,
height: String,
})
// 定义触发的自定义事件
const emit = defineEmits(['close', 'changed'])
const emit = defineEmits(['close', 'changed', 'levelup', 'leveldown'])
// 按钮点击
function onClose() {
emit('close')
}
// 上升优先级
function onLevelUp() {
emit('levelup', props.pri)
}
// 下降优先级
function onLevelDown() {
emit('leveldown', props.pri)
}
// 选项变化
function filtersChanged(value: string[]) {
emit('changed', props.pri, value)
@@ -24,6 +35,11 @@ function filtersChanged(value: string[]) {
const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '特效字幕', value: ' SPECSUB ' },
{ title: '中文字幕', value: ' CNSUB ' },
{ title: '国语配音', value: ' CNVOI ' },
{ title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '粤语配音', value: ' HKVOI ' },
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
{ title: '促销: 免费', value: ' FREE ' },
{ title: '分辨率: 4K', value: ' 4K ' },
{ title: '分辨率: 1080P', value: ' 1080P ' },
{ title: '分辨率: 720P', value: ' 720P ' },
@@ -38,22 +54,41 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '排除: REMUX', value: ' !REMUX ' },
{ title: '质量: WEB-DL', value: ' WEBDL ' },
{ title: '排除: WEB-DL', value: ' !WEBDL ' },
{ title: '质量: 60fps', value: ' 60FPS ' },
{ title: '排除: 60fps', value: ' !60FPS ' },
{ title: '编码: H265', value: ' H265 ' },
{ title: '排除: H265', value: ' !H265 ' },
{ title: '编码: H264', value: ' H264 ' },
{ title: '排除: H264', value: ' !H264 ' },
{ title: '效果: 杜比视界', value: ' DOLBY ' },
{ title: '排除: 杜比视界', value: ' !DOLBY ' },
{ title: '效果: 杜比全景声', value: ' ATMOS ' },
{ title: '排除: 杜比全景声', value: ' !ATMOS ' },
{ title: '效果: HDR', value: ' HDR ' },
{ title: '排除: HDR', value: ' !HDR ' },
{ title: '国语配音', value: ' CNVOI ' },
{ title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '促销: 免费', value: ' FREE ' },
{ title: '效果: SDR', value: ' SDR ' },
{ title: '排除: SDR', value: ' !SDR ' },
{ title: '效果: 3D', value: ' 3D ' },
{ title: '排除: 3D', value: ' !3D ' },
])
</script>
<template>
<VCard variant="tonal" :width="props.width" :height="props.height">
<span class="absolute top-3 right-14">
<IconBtn
v-if="props.pri !== '1'"
@click.stop="onLevelUp"
>
<VIcon icon="mdi-arrow-up" />
</IconBtn>
<IconBtn
v-if="props.pri !== props.maxpri"
@click.stop="onLevelDown"
>
<VIcon icon="mdi-arrow-down" />
</IconBtn>
</span>
<DialogCloseBtn @click="onClose" />
<VCardItem>
<VCardTitle>优先级 {{ props.pri }}</VCardTitle>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { PropType, Ref } from 'vue'
import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
@@ -39,6 +40,12 @@ const seasonsNotExisted = ref<{ [key: number]: number }>({})
// 订阅季弹窗
const subscribeSeasonDialog = ref(false)
// 订阅编辑弹窗
const subscribeEditDialog = ref(false)
// 订阅ID
const subscribeId = ref(0)
// 季详情
const seasonInfos = ref<TmdbSeason[]>([])
@@ -86,6 +93,7 @@ async function handleAddSubscribe() {
}
else {
// 弹出季选择列表,支持多选
seasonsSelected.value = []
subscribeSeasonDialog.value = true
}
}
@@ -112,7 +120,7 @@ async function addSubscribe(season = 0) {
// 全部存在时洗版
best_version = !seasonsNotExisted.value[season] ? 1 : 0
// 请求API
const result: { [key: string]: any } = await api.post('subscribe', {
const result: { [key: string]: any } = await api.post('subscribe/', {
name: props.media?.title,
type: props.media?.type,
year: props.media?.year,
@@ -136,6 +144,12 @@ async function addSubscribe(season = 0) {
result.message,
best_version,
)
// 弹出订阅编辑弹窗
if (result.success && seasonsSelected.value.length <= 1) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
}
catch (error) {
console.error(error)
@@ -156,9 +170,9 @@ function showSubscribeAddToast(result: boolean,
if (best_version > 0)
subname = '洗版订阅'
if (result)
if (result && seasonsSelected.value.length > 1)
$toast.success(`${title} 添加${subname}成功!`)
else
else if (!result)
$toast.error(`${title} 添加${subname}失败:${message}`)
}
@@ -378,6 +392,21 @@ function getSeasonPoster(posterPath: string) {
return ''
return `https://image.tmdb.org/t/p/w500${posterPath}`
}
// 将yyyy-mm-dd转换为yyyy年mm月dd日
function formatAirDate(airDate: string) {
if (!airDate)
return ''
const date = new Date(airDate)
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`
}
// 从yyyy-mm-dd中提取年份
function getYear(airDate: string) {
if (!airDate)
return ''
const date = new Date(airDate)
return date.getFullYear()
}
</script>
<template>
@@ -465,18 +494,24 @@ function getSeasonPoster(posterPath: string) {
inset
scrollable
>
<VCard title="选择订阅季">
<VCard class="rounded-t">
<DialogCloseBtn @click="subscribeSeasonDialog = false" />
<VCardText class="p-0">
<VCardTitle class="pe-10">
订阅 - {{ props.media?.title }}
</VCardTitle>
<VCardText>
<VList
v-model="seasonsSelected"
multiple
v-model:selected="seasonsSelected"
lines="three"
select-strategy="classic"
>
<VListItem v-for="(item, i) in seasonInfos" :key="i">
<VListItem
v-for="(item, i) in seasonInfos" :key="i"
:value="item"
>
<template #prepend>
<VImg
height="80"
height="90"
width="60"
:src="getSeasonPoster(item.poster_path || '')"
aspect-ratio="2/3"
@@ -493,10 +528,20 @@ function getSeasonPoster(posterPath: string) {
<VListItemTitle>
{{ item.season_number }}
</VListItemTitle>
<VListItemSubtitle class="mt-1">
评分{{ item.vote_average }}上映日期{{ item.air_date }}
<VListItemSubtitle class="mt-1 me-2">
<VChip
v-if="item.vote_average"
color="primary"
size="small"
class="mb-1"
>
<VIcon icon="mdi-star" /> {{ item.vote_average }}
</VChip>
{{ getYear(item.air_date || '') }} {{ item.episode_count }}
</VListItemSubtitle>
<VListItemSubtitle>
{{ media?.title }} {{ item.season_number }} 季于 {{ formatAirDate(item.air_date || '') }} 首播
</VListItemSubtitle>
<VListItemSubtitle v-html="item.overview" />
<VListItemSubtitle>
<VChip
v-if="seasonsNotExisted"
@@ -507,9 +552,9 @@ function getSeasonPoster(posterPath: string) {
{{ getExistText(item.season_number || 0) }}
</VChip>
</VListItemSubtitle>
<template #append>
<template #append="{ isSelected }">
<VListItemAction start>
<VSwitch v-model="seasonsSelected" :value="item.season_number" />
<VSwitch :model-value="isSelected" />
</VListItemAction>
</template>
</VListItem>
@@ -526,6 +571,14 @@ function getSeasonPoster(posterPath: string) {
</div>
</VCard>
</VBottomSheet>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"
@save="subscribeEditDialog = false"
@remove="() => { subscribeEditDialog = false; handleCheckSubscribe(); }"
/>
</template>
<style lang="scss">

View File

@@ -57,7 +57,7 @@ async function installPlugin() {
:class="{ shadow: isImageLoaded }"
>
<VImg
:src="`/plugin/${props.plugin?.plugin_icon}`"
:src="`/plugin_icon/${props.plugin?.plugin_icon}`"
aspect-ratio="4/3"
cover
@load="isImageLoaded = true"

View File

@@ -216,7 +216,7 @@ const dropdownItems = ref([
:class="{ shadow: isImageLoaded }"
>
<VImg
:src="`/plugin/${props.plugin?.plugin_icon}`"
:src="`/plugin_icon/${props.plugin?.plugin_icon}`"
aspect-ratio="4/3"
cover
:class="{ shadow: isImageLoaded }"
@@ -234,13 +234,15 @@ const dropdownItems = ref([
</VCardText>
</VCard>
<!-- 插件配置页面 -->
<VDialog
<VBottomSheet
v-model="pluginConfigDialog"
max-width="50rem"
scrollable
persistent
inset
>
<VCard :title="`${props.plugin?.plugin_name} - 配置`">
<VCard
:title="`${props.plugin?.plugin_name} - 配置`"
class="rounded-t"
>
<DialogCloseBtn @click="pluginConfigDialog = false" />
<VCardText>
<FormRender
@@ -255,21 +257,26 @@ const dropdownItems = ref([
查看详情
</VBtn>
<VSpacer />
<VBtn @click="savePluginConf">
<VBtn
variant="tonal"
@click="savePluginConf"
>
保存
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</VBottomSheet>
<!-- 插件详情页面 -->
<VDialog
<VBottomSheet
v-model="pluginInfoDialog"
max-width="62.5rem"
scrollable
persistent
inset
>
<VCard :title="`${props.plugin?.plugin_name}`">
<VCard
:title="`${props.plugin?.plugin_name}`"
class="rounded-t"
>
<DialogCloseBtn @click="pluginInfoDialog = false" />
<VCardText>
<PageRender
@@ -279,16 +286,21 @@ const dropdownItems = ref([
/>
</VCardText>
<VCardActions>
<VBtn @click="showPluginConfig">
<VBtn
@click="showPluginConfig"
>
配置
</VBtn>
<VSpacer />
<VBtn @click="pluginInfoDialog = false">
<VBtn
variant="tonal"
@click="pluginInfoDialog = false"
>
关闭
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</VBottomSheet>
</template>
<style lang="scss" scoped>

View File

@@ -1,8 +1,9 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification'
import SiteAddEditForm from '../form/SiteAddEditForm.vue'
import { formatFileSize } from '@core/utils/formatters'
import { numberValidator, requiredValidator } from '@/@validators'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { Site, TorrentInfo } from '@/api/types'
import ExistIcon from '@core/components/ExistIcon.vue'
@@ -15,7 +16,7 @@ const cardProps = defineProps({
})
// 定义触发的自定义事件
const emit = defineEmits(['remove', 'update'])
const emit = defineEmits(['update', 'remove'])
// 密码输入
const isPasswordVisible = ref(false)
@@ -42,7 +43,7 @@ const updateButtonDisable = ref(false)
const siteCookieDialog = ref(false)
// 站点编辑弹窗
const siteInfoDialog = ref(false)
const siteEditDialog = ref(false)
// 资源浏览弹窗
const resourceDialog = ref(false)
@@ -78,27 +79,6 @@ const userPwForm = ref({
password: '',
})
// 状态下拉项
const statusItems = [
{ title: '启用', value: true },
{ title: '停用', value: false },
]
// 生成1到50的优先级下拉框选项
const priorityItems = ref(
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
title: item,
value: item,
})),
)
// 站点编辑表单数据
const siteForm = reactive<any>(cardProps.site ?? {})
// 类型转换
siteForm.proxy = siteForm.proxy === 1
siteForm.render = siteForm.render === 1
// 打开种子详情页面
function openTorrentDetail(page_url: string) {
window.open(page_url, '_blank')
@@ -144,11 +124,6 @@ async function handleSiteUpdate() {
siteCookieDialog.value = true
}
// 打开站点编辑弹窗
async function handleSiteInfo() {
siteInfoDialog.value = true
}
// 打开资源浏览弹窗
async function handleResourceBrowse() {
resourceDialog.value = true
@@ -189,42 +164,6 @@ async function updateSiteCookie() {
}
}
// 调用API删除站点信息
async function deleteSiteInfo() {
try {
siteInfoDialog.value = false
const result: { [key: string]: any } = await api.delete(`site/${cardProps.site?.id}`)
if (result.success) {
$toast.success(`${cardProps.site?.name} 删除成功!`)
emit('remove')
}
else { $toast.error(`${cardProps.site?.name} 删除失败:${result.message}`) }
}
catch (error) {
$toast.error(`${cardProps.site?.name} 删除失败!`)
console.error(error)
}
}
// 调用API更新站点信息
async function updateSiteInfo() {
try {
// 更新按钮状态
siteInfoDialog.value = false
const result: { [key: string]: any } = await api.put('site', siteForm)
if (result.success) {
$toast.success(`${cardProps.site?.name} 更新成功!`)
emit('update')
}
else { $toast.error(`${cardProps.site?.name} 更新失败:${result.message}`) }
}
catch (error) {
$toast.error(`${cardProps.site?.name} 更新失败!`)
console.error(error)
}
}
// 促销Chip类
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
if (downloadVolume === 0)
@@ -264,9 +203,9 @@ onMounted(() => {
<VCard
:height="cardProps.height"
:width="cardProps.width"
:flat="!siteForm.is_active"
:flat="!cardProps.site?.is_active"
class="overflow-hidden"
@click="handleSiteInfo"
@click="siteEditDialog = true"
>
<template #image>
<VAvatar
@@ -278,17 +217,19 @@ onMounted(() => {
</VAvatar>
</template>
<VCardItem>
<VCardTitle class="font-bold" @click.stop="openSitePage">
{{ cardProps.site?.name }}
<VCardTitle class="font-bold">
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
</VCardTitle>
<VCardSubtitle>{{ cardProps.site?.url }}</VCardSubtitle>
<VCardSubtitle>
{{ cardProps.site?.url }}
</VCardSubtitle>
</VCardItem>
<ExistIcon v-if="siteForm.is_active" />
<ExistIcon v-if="cardProps.site?.is_active" />
<VCardText class="py-2">
<VTooltip
v-if="siteForm.render"
v-if="cardProps.site?.render === 1"
text="浏览器仿真"
>
<template #activator="{ props }">
@@ -302,7 +243,7 @@ onMounted(() => {
</VTooltip>
<VTooltip
v-if="siteForm.proxy"
v-if="cardProps.site?.proxy === 1"
text="代理"
>
<template #activator="{ props }">
@@ -316,7 +257,7 @@ onMounted(() => {
</VTooltip>
<VTooltip
v-if="siteForm.limit_interval"
v-if="cardProps.site?.limit_interval"
text="流控"
>
<template #activator="{ props }">
@@ -330,7 +271,7 @@ onMounted(() => {
</VTooltip>
<VTooltip
v-if="siteForm.filter"
v-if="cardProps.site?.filter"
text="过滤"
>
<template #activator="{ props }">
@@ -419,143 +360,22 @@ onMounted(() => {
<VCardActions>
<VSpacer />
<VBtn @click="updateSiteCookie">
<VBtn
variant="tonal"
@click="updateSiteCookie"
>
开始更新
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 站点编辑弹窗 -->
<VDialog
v-model="siteInfoDialog"
max-width="50rem"
persistent
scrollable
>
<!-- Dialog Content -->
<VCard :title="`编辑站点 - ${cardProps.site?.name}`">
<VCardText class="pt-2">
<DialogCloseBtn @click="siteInfoDialog = false" />
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="siteForm.url"
label="站点地址"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.pri"
label="优先级"
:items="priorityItems"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.is_active"
:items="statusItems"
label="状态"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"
label="站点Cookie"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="siteForm.ua"
label="站点User-Agent"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_interval"
label="单位周期(秒)"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问次数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.proxy"
label="代理"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.render"
label="仿真"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn color="error" @click="deleteSiteInfo">
删除
</VBtn>
<VSpacer />
<VBtn @click="updateSiteInfo">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<SiteAddEditForm
v-model="siteEditDialog"
:siteid="cardProps.site?.id"
@save="siteEditDialog = false; emit('update')"
@remove="emit('remove')"
@close="siteEditDialog = false"
/>
<!-- 站点资源弹窗 -->
<VDialog
v-model="resourceDialog"
@@ -647,13 +467,14 @@ onMounted(() => {
<VListItemTitle>查看详情</VListItemTitle>
</VListItem>
<VListItem
v-if="item.raw.enclosure?.startsWith('http')"
variant="plain"
@click="downloadTorrentFile(item.raw.enclosure)"
>
<template #prepend>
<VIcon icon="mdi-download" />
</template>
<VListItemTitle>下载种子</VListItemTitle>
<VListItemTitle>下载种子文件</VListItemTitle>
</VListItem>
</VList>
</VMenu>

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
import { calculateTimeDifference } from '@/@core/utils'
import { formatSeason } from '@/@core/utils/formatters'
import { numberValidator } from '@/@validators'
import api from '@/api'
import type { Site, Subscribe } from '@/api/types'
import type { Subscribe } from '@/api/types'
// 输入参数
const props = defineProps({
@@ -21,19 +21,7 @@ const $toast = useToast()
const imageLoaded = ref(false)
// 订阅弹窗
const subscribeInfoDialog = ref(false)
// 站点数据列表
const siteList = ref<Site[]>([])
// 站点选择下载框
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
// 订阅编辑表单
const subscribeForm = reactive<any>(props.media ?? {})
// 类型转换
subscribeForm.best_version = subscribeForm.best_version === 1
const subscribeEditDialog = ref(false)
// 上一次更新时间
const lastUpdateText = ref(
@@ -114,58 +102,9 @@ async function searchSubscribe() {
}
}
// 调用API修改订阅
async function updateSubscribeInfo() {
subscribeInfoDialog.value = false
try {
const result: { [key: string]: any } = await api.put('subscribe', subscribeForm)
// 提示
if (result.success) {
$toast.success(`${props.media?.name} 更新成功!`)
// 通知父组件刷新
emit('remove')
}
else { $toast.error(`${props.media?.name} 更新失败:${result.message}`) }
}
catch (e) {
console.log(e)
}
}
// 获取站点列表数据
async function loadSites() {
try {
const data: Site[] = await api.get('site/rss')
// 过滤站点,只有启用的站点才显示
siteList.value = data.filter(item => item.is_active)
}
catch (error) {
console.error(error)
}
}
// 获取站点列表选择框数据
async function getSiteList() {
// 加载订阅站点列表
if (!siteList.value.length)
await loadSites()
const maps = siteList.value.map((item) => {
return {
title: item.name,
value: item.id,
}
})
selectSitesOptions.value = maps.flat()
}
// 编辑订阅响应
async function editSubscribeDialog() {
await getSiteList()
subscribeInfoDialog.value = true
subscribeEditDialog.value = true
}
// 弹出菜单
@@ -201,7 +140,7 @@ const dropdownItems = ref([
<template>
<VCard
:key="props.media?.id"
:class="`${subscribeForm.best_version ? 'outline-dashed outline-1' : ''}`"
:class="`${props.media?.best_version ? 'outline-dashed outline-1' : ''}`"
@click="editSubscribeDialog"
>
<template #image>
@@ -323,100 +262,11 @@ const dropdownItems = ref([
/>
</VCard>
<!-- 订阅编辑弹窗 -->
<VDialog
v-model="subscribeInfoDialog"
max-width="50rem"
persistent
scrollable
>
<!-- Dialog Content -->
<VCard :title="`订阅 - ${props.media?.name}`">
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="subscribeForm.keyword"
label="搜索关键词"
/>
</VCol>
<VCol
v-if="props.media?.type === '电视剧'"
cols="12"
md="3"
>
<VTextField
v-model="subscribeForm.total_episode"
label="总集数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
v-if="props.media?.type === '电视剧'"
cols="12"
md="3"
>
<VTextField
v-model="subscribeForm.start_episode"
label="开始集数"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="subscribeForm.include"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="subscribeForm.exclude"
label="排除(关键字、正则式)"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VSelect
v-model="subscribeForm.sites"
:items="selectSitesOptions"
chips
label="订阅站点"
multiple
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VSwitch
v-model="subscribeForm.best_version"
label="洗版"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn @click="subscribeInfoDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn @click="updateSubscribeInfo">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<SubscribeEditForm
v-model="subscribeEditDialog"
:subid="props.media?.id"
@remove="() => { emit('remove');subscribeEditDialog = false; }"
@save="() => { emit('save');subscribeEditDialog = false; }"
@close="subscribeEditDialog = false"
/>
</template>

View File

@@ -20,6 +20,9 @@ const keyword = ref('')
// 加载中
const loading = ref(false)
// ref
const tmdbKeyword = ref<HTMLElement | null>(null)
// 选中条目
function selectMedia(item: TmdbItem) {
console.log(item)
@@ -68,6 +71,14 @@ async function searchMedias() {
console.error(e)
}
}
// 加载时聚焦搜索框
onMounted(() => {
// 500ms后聚焦
setTimeout(() => {
tmdbKeyword.value?.focus()
}, 500)
})
</script>
<template>
@@ -77,11 +88,15 @@ async function searchMedias() {
>
<VToolbar flat class="p-0">
<VTextField
ref="tmdbKeyword"
v-model="keyword"
label="输入名称搜索"
class="mx-3"
variant="underlined"
single-line
placeholder="电影或电视剧名称"
variant="solo"
append-inner-icon="mdi-magnify"
flat
class="mx-1"
:loading="loading"
@click:append-inner="searchMedias"
@keydown.enter="searchMedias"

View File

@@ -64,6 +64,9 @@ async function handleAddDownload(_site: any = undefined,
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
variant: 'tonal',
},
})
if (!isConfirmed)
@@ -76,7 +79,7 @@ async function handleAddDownload(_site: any = undefined,
async function addDownload(_media: any, _torrent: any) {
startNProgress()
try {
const result: { [key: string]: any } = await api.post('download', {
const result: { [key: string]: any } = await api.post('download/', {
media_in: _media,
torrent_in: _torrent,
})
@@ -122,26 +125,6 @@ function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
onMounted(() => {
getSiteIcon()
})
// 弹出菜单
const dropdownItems = ref([
{
title: '查看详情',
value: 1,
props: {
prependIcon: 'mdi-information',
click: openTorrentDetail,
},
},
{
title: '下载种子',
value: 2,
props: {
prependIcon: 'mdi-download',
click: downloadTorrentFile,
},
},
])
</script>
<template>
@@ -180,15 +163,23 @@ const dropdownItems = ref([
>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
:key="i"
variant="plain"
@click="item.props.click"
@click="openTorrentDetail()"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
<VIcon icon="mdi-information" />
</template>
<VListItemTitle v-text="item.title" />
<VListItemTitle>查看详情</VListItemTitle>
</VListItem>
<VListItem
v-if="props.torrent?.torrent_info?.enclosure?.startsWith('http')"
variant="plain"
@click="downloadTorrentFile()"
>
<template #prepend>
<VIcon icon="mdi-download" />
</template>
<VListItemTitle>下载种子文件</VListItemTitle>
</VListItem>
</VList>
</VMenu>

View File

@@ -0,0 +1,251 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { formatFileSize } from '@/@core/utils/formatters'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { Context } from '@/api/types'
// 输入参数
const props = defineProps({
torrent: Object as PropType<Context>,
})
// 提示框
const $toast = useToast()
// 确认框
const createConfirm = useConfirm()
// 更多来源界面
const showMoreTorrents = ref(false)
// 种子信息
const torrent = ref(props.torrent?.torrent_info)
// 媒体信息
const media = ref(props.torrent?.media_info)
// 识别元数据
const meta = ref(props.torrent?.meta_info)
// 站点图标
const siteIcon = ref('')
// 查询站点图标
async function getSiteIcon() {
try {
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
}
catch (error) {
console.error(error)
}
}
// 询问并添加下载
async function handleAddDownload(_site: any = undefined,
_media: any = undefined,
_torrent: any = undefined) {
if (!_media || !_torrent || !_site) {
_site = torrent.value?.site_name
_media = media.value
_torrent = torrent.value
}
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认下载【${_site}${_torrent?.title} ?`,
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
variant: 'tonal',
},
})
if (!isConfirmed)
return
addDownload(_media, _torrent)
}
// 添加下载
async function addDownload(_media: any, _torrent: any) {
startNProgress()
try {
const result: { [key: string]: any } = await api.post('download/', {
media_in: _media,
torrent_in: _torrent,
})
if (result.success) {
// 添加下载成功
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
}
else {
// 添加下载失败
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
}
}
catch (error) {
console.error(error)
}
doneNProgress()
}
// 打开种子详情页面
function openTorrentDetail() {
window.open(torrent.value?.page_url, '_blank')
}
// 下载种子文件
async function downloadTorrentFile() {
window.open(torrent.value?.enclosure, '_blank')
}
// 促销Chip类
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
if (downloadVolume === 0)
return 'text-white bg-lime-500'
else if (downloadVolume < 1)
return 'text-white bg-green-500'
else if (uploadVolume !== 1)
return 'text-white bg-sky-500'
else
return 'text-white bg-gray-500'
}
// 装载时查询站点图标
onMounted(() => {
getSiteIcon()
})
</script>
<template>
<VListItem @click="handleAddDownload">
<template
v-if="!showMoreTorrents"
#prepend
>
<VAvatar
class="rounded"
variant="flat"
@click.stop="openTorrentDetail"
>
<VImg :src="siteIcon" />
</VAvatar>
</template>
<VListItemTitle class="break-words overflow-visible whitespace-break-spaces">
{{ torrent?.title }}
<span class="text-green-700 ms-2 text-sm">{{ torrent?.seeders }}</span>
<span class="text-orange-700 ms-2 text-sm">{{ torrent?.peers }}</span>
</VListItemTitle>
<VListItemSubtitle>
{{ torrent?.description }}
</VListItemSubtitle>
<div
v-if="torrent?.labels"
class="pt-2"
>
<VChip
v-for="(label, index) in torrent?.labels"
:key="index"
variant="elevated"
size="small"
color="primary"
class="me-1 mb-1"
>
{{ label }}
</VChip>
<VChip
v-if="meta?.edition"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-red-500"
>
{{ meta?.edition }}
</VChip>
<VChip
v-if="meta?.resource_pix"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-red-500"
>
{{ meta?.resource_pix }}
</VChip>
<VChip
v-if="meta?.video_encode"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-orange-500"
>
{{ meta?.video_encode }}
</VChip>
<VChip
v-if="torrent?.size"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-yellow-500"
>
{{ formatFileSize(torrent?.size) }}
</VChip>
<VChip
v-if="meta?.resource_team"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-cyan-500"
>
{{ meta?.resource_team }}
</VChip>
<VChip
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
:class="
getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)
"
variant="elevated"
size="small"
class="me-1 mb-1"
>
{{ torrent?.volume_factor }}
</VChip>
</div>
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
variant="plain"
@click="openTorrentDetail()"
>
<template #prepend>
<VIcon icon="mdi-information" />
</template>
<VListItemTitle>查看详情</VListItemTitle>
</VListItem>
<VListItem
v-if="props.torrent?.torrent_info?.enclosure?.startsWith('http')"
variant="plain"
@click="downloadTorrentFile()"
>
<template #prepend>
<VIcon icon="mdi-download" />
</template>
<VListItemTitle>下载种子文件</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
</VListItem>
</template>

View File

@@ -4,13 +4,12 @@ import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog'
import axios from 'axios'
import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators'
import ReorganizeForm from '../form/ReorganizeForm.vue'
import { formatBytes } from '@core/utils/formatters'
import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store'
import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
// 输入参数
const inProps = defineProps({
@@ -32,6 +31,15 @@ const $toast = useToast()
// 是否正在加载
const loading = ref(true)
// 识别进度条
const progressDialog = ref(false)
// 识别进度文本
const progressText = ref('请稍候 ...')
// 识别进度
const progressValue = ref(0)
// 确认框
const createConfirm = useConfirm()
@@ -53,57 +61,18 @@ const renamePopper = ref(false)
// 整理弹窗
const transferPopper = ref(false)
// 整理进度条
const progressDialog = ref(false)
// 整理进度文本
const progressText = ref('请稍候 ...')
// 整理进度
const progressValue = ref(0)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 新名称
const newName = ref('')
// 当前名称
const currentItem = ref<FileItem>()
// 文件转移表单
const transferForm = reactive({
path: '',
target: '',
tmdbid: null,
season: null,
type_name: '',
transfer_type: '',
episode_format: '',
episode_detail: '',
episode_part: '',
episode_offset: null,
min_filesize: 0,
})
// 识别结果
const nameTestResult = ref<Context>()
// 识别结果对话框
const nameTestDialog = ref(false)
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 生成1到50季的下拉框选项
const seasonItems = ref(
Array.from({ length: 51 }, (_, i) => i).map(item => ({
title: `${item}`,
value: item,
})),
)
// 目录过滤
const dirs = computed(() =>
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
@@ -158,6 +127,9 @@ async function deleteItem(item: FileItem) {
dialogProps: {
maxWidth: '50rem',
},
cancellationButtonProps: {
variant: 'tonal',
},
})
if (confirmed) {
@@ -245,41 +217,6 @@ function showTransfer(item: FileItem) {
transferPopper.value = true
}
// 整理文件
async function transfer() {
transferForm.path = currentItem.value?.path ?? ''
// 开始整理文件
try {
// 关闭弹窗
transferPopper.value = false
// 显示进度条
progressDialog.value = true
// 开始监听进度
startLoadingProgress()
// 异步调API结束后关闭进度条
api.post('transfer/manual', {}, {
params: transferForm,
}).then((res: any) => {
// 关闭进度条
progressDialog.value = false
// 停止监听进度
stopLoadingProgress()
// 显示结果
if (res.success) {
$toast.success(`${currentItem.value?.name} 整理完成!`)
// 重新加载
load()
}
else {
$toast.error(`${currentItem.value?.name} 整理失败:${res.message}`)
}
})
}
catch (e) {
console.log(e)
}
}
// 将文件修改时间timestape转换为本地时间
function formatTime(timestape: number) {
return new Date(timestape * 1000).toLocaleString()
@@ -307,29 +244,6 @@ watch(
},
)
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '请稍候 ...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
)
progressEventSource.value.onmessage = (event) => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
}
}
}
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
// 调用API识别
async function recognize(path: string) {
try {
@@ -586,23 +500,19 @@ onMounted(() => {
v-model="renamePopper"
max-width="50rem"
>
<template #activator="{ props }">
<IconBtn title="重命名" v-bind="props">
<VIcon icon="mdi-rename-outline" />
</IconBtn>
</template>
<VCard title="重命名">
<VCardText>
<VTextField v-model="newName" label="名称" />
</VCardText>
<VCardActions>
<div class="flex-grow-1" />
<VBtn depressed @click="renamePopper = false">
取消
</VBtn>
<VSpacer />
<VBtn
:disabled="!newName"
depressed
variant="tonal"
@click="rename"
>
重命名
@@ -611,178 +521,44 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 文件整理弹窗 -->
<VDialog
<ReorganizeForm
v-model="transferPopper"
max-width="50rem"
scrollable
>
<template #activator="{ props }">
<IconBtn title="整理" v-bind="props">
<VIcon icon="mdi-folder-arrow-right-outline" />
</IconBtn>
</template>
<VCard :title="`文件整理 - ${currentItem?.name}`">
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="8"
>
<VTextField
v-model="transferForm.target"
label="目的路径"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="transferForm.transfer_type"
label="整理方式"
:items="[
{ title: '默认', value: '' },
{ title: '移动', value: 'move' },
{ title: '复制', value: 'copy' },
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="transferForm.type_name"
label="类型"
:items="[{ title: '请选择', value: '' }, { title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="transferForm.tmdbid"
label="TMDBID"
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-show="transferForm.type_name === '电视剧'"
v-model.number="transferForm.season"
label="季"
:items="seasonItems"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="8">
<VTextField
v-model="transferForm.episode_format"
label="集数定位"
placeholder="使用{ep}定位集数"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_detail"
label="指定集数"
placeholder="起始集,终止集如1或1,2"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_part"
label="指定Part"
placeholder="如part1"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.episode_offset"
label="集数偏移"
placeholder="如-10"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.min_filesize"
label="最小文件大小MB"
:rules="[numberValidator]"
placeholder="0"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn depressed @click="transferPopper = false">
取消
</VBtn>
<VSpacer />
<VBtn
@click="transfer"
>
开始整理
</VBtn>
</VCardActions>
</VCard>
</VDialog>
:path="currentItem?.path"
@done="transferPopper = false; load()"
@close="transferPopper = false"
/>
<!-- 手动整理进度框 -->
<vDialog
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<vCard
<VCard
color="primary"
>
<vCardText class="text-center">
<VCardText class="text-center">
{{ progressText }}
<vProgressLinear
<VProgressLinear
v-if="progressValue"
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</vCardText>
</vCard>
</vDialog>
</VCardText>
</VCard>
</VDialog>
<!-- 识别结果对话框 -->
<vDialog
<VDialog
v-model="nameTestDialog"
width="50rem"
>
<vCard>
<VCard>
<DialogCloseBtn @click="nameTestDialog = false" />
<VCardItem>
<MediaInfoCard :context="nameTestResult" />
</VCardItem>
</vCard>
</vDialog>
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="40rem"
scrollable
>
<TmdbSelectorCard
v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false"
/>
</vDialog>
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>

View File

@@ -171,6 +171,7 @@ const sortIcon = computed(() => {
<VBtn
:disabled="!newFolderName"
depressed
variant="tonal"
@click="mkdir"
>
新建

View File

@@ -0,0 +1,309 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import TmdbSelectorCard from '../cards/TmdbSelectorCard.vue'
import store from '@/store'
import api from '@/api'
import { numberValidator } from '@/@validators'
// 输入参数
const props = defineProps({
path: String,
target: String,
logids: Array<number>,
})
// 定义事件
const emit = defineEmits(['done', 'close'])
// 生成1到50季的下拉框选项
const seasonItems = ref(
Array.from({ length: 51 }, (_, i) => i).map(item => ({
title: `${item}`,
value: item,
})),
)
// 提示框
const $toast = useToast()
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 整理进度条
const progressDialog = ref(false)
// 整理进度文本
const progressText = ref('请稍候 ...')
// 整理进度
const progressValue = ref(0)
// 文件转移表单
const transferForm = reactive({
logid: 0,
path: '',
target: props.target ?? '',
tmdbid: null,
season: null,
type_name: '',
transfer_type: '',
episode_format: '',
episode_detail: '',
episode_part: '',
episode_offset: null,
min_filesize: 0,
})
watchEffect(() => {
if (props.path)
transferForm.path = props.path
if (props.target)
transferForm.target = props.target
})
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '请稍候 ...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
)
progressEventSource.value.onmessage = (event) => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
}
}
}
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
// 整理文件
// eslint-disable-next-line sonarjs/cognitive-complexity
async function transfer() {
if (!props.logids && !props.path)
return
// 显示进度条
progressDialog.value = true
// 开始监听进度
startLoadingProgress()
if (props.path) {
// 文件整理
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
params: transferForm,
})
// 显示结果
if (result.success)
$toast.success(`${props.path} 整理完成!`)
else
$toast.error(`${props.path} 整理失败:${result.message}`)
}
catch (e) {
console.log(e)
}
}
else if (props.logids) {
// 日志整理
for (const logid of props.logids) {
transferForm.logid = logid
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
params: transferForm,
})
if (!result.success)
$toast.error(`历史记录 ${logid} 重新整理失败:${result.message}`)
}
catch (e) {
console.log(e)
}
}
}
// 停止监听进度
stopLoadingProgress()
// 关闭进度条
progressDialog.value = false
// 重新加载
emit('done')
}
</script>
<template>
<VBottomSheet
scrollable
inset
>
<VCard
:title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="8"
>
<VTextField
v-model="transferForm.target"
label="目的路径"
placeholder="留空自动"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="transferForm.transfer_type"
label="整理方式"
:items="[
{ title: '默认', value: '' },
{ title: '移动', value: 'move' },
{ title: '复制', value: 'copy' },
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
{ title: 'Rclone复制', value: 'rclone_copy' },
{ title: 'Rclone移动', value: 'rclone_move' },
]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="transferForm.type_name"
label="类型"
:items="[{ title: '自动', value: '' }, { title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="transferForm.tmdbid"
:disabled="transferForm.type_name === ''"
label="TMDBID"
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-show="transferForm.type_name === '电视剧'"
v-model.number="transferForm.season"
label="季"
:items="seasonItems"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="8">
<VTextField
v-model="transferForm.episode_format"
label="集数定位"
placeholder="使用{ep}定位集数"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_detail"
label="指定集数"
placeholder="起始集,终止集如1或1,2"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_part"
label="指定Part"
placeholder="如part1"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.episode_offset"
label="集数偏移"
placeholder="如-10"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.min_filesize"
label="最小文件大小MB"
:rules="[numberValidator]"
placeholder="0"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn depressed @click="emit('close')">
取消
</VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="transfer"
>
开始整理
</VBtn>
</VCardActions>
</VCard>
<!-- 手动整理进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
v-if="progressValue"
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</VCardText>
</VCard>
</VDialog>
<!-- TMDB ID搜索框 -->
<VDialog
v-model="tmdbSelectorDialog"
width="40rem"
scrollable
>
<TmdbSelectorCard
v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false"
/>
</VDialog>
</VBottomSheet>
</template>

View File

@@ -0,0 +1,274 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import type { Site } from '@/api/types'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api'
// 输入参数
const props = defineProps({
siteid: Number,
oper: String,
})
// 注册事件
const emit = defineEmits(['save', 'remove', 'close'])
// 站点编辑表单数据
const siteForm = ref<Site>({
id: props.siteid ?? 0,
url: '',
rss: '',
cookie: '',
ua: '',
pri: 0,
is_active: true,
limit_interval: 0,
limit_seconds: 0,
name: '',
domain: '',
})
// 提示框
const $toast = useToast()
// 状态下拉项
const statusItems = [
{ title: '启用', value: true },
{ title: '停用', value: false },
]
// 生成1到50的优先级下拉框选项
const priorityItems = ref(
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
title: item,
value: item,
})),
)
// 监控输入参数
watchEffect(async () => {
if (props.siteid)
fetchSiteInfo()
})
// 查询站点信息
async function fetchSiteInfo() {
try {
siteForm.value = await api.get(`site/${props.siteid}`)
siteForm.value.proxy = siteForm.value.proxy === 1
siteForm.value.render = siteForm.value.render === 1
}
catch (error) {
console.error(error)
}
}
// 调用API 新增站点
async function addSite() {
if (!siteForm.value?.url)
return
startNProgress()
try {
const result: { [key: string]: string } = await api.post('site/', siteForm.value)
if (result.success) {
$toast.success('新增站点成功')
emit('save')
}
else { $toast.error(`新增站点失败:${result.message}`) }
}
catch (error) {
console.error(error)
}
doneNProgress()
}
// 调用API删除站点信息
async function deleteSiteInfo() {
try {
const result: { [key: string]: any } = await api.delete(`site/${siteForm.value?.id}`)
if (result.success)
emit('remove')
else $toast.error(`${siteForm.value?.name} 删除失败:${result.message}`)
}
catch (error) {
$toast.error(`${siteForm.value?.name} 删除失败!`)
console.error(error)
}
}
// 调用API更新站点信息
async function updateSiteInfo() {
startNProgress()
try {
const result: { [key: string]: any } = await api.put('site/', siteForm.value)
if (result.success) {
$toast.success(`${siteForm.value?.name} 更新成功!`)
emit('save')
}
else { $toast.error(`${siteForm.value?.name} 更新失败:${result.message}`) }
}
catch (error) {
$toast.error(`${siteForm.value?.name} 更新失败!`)
console.error(error)
}
doneNProgress()
}
</script>
<template>
<VBottomSheet
inset
scrollable
>
<VCard
:title="`${props.oper === 'add' ? '新增' : '编辑'}站点${props.oper !== 'add' ? ` - ${siteForm.name}` : ''}`"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="siteForm.url"
label="站点地址"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.pri"
label="优先级"
:items="priorityItems"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.is_active"
:items="statusItems"
label="状态"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"
label="站点Cookie"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="siteForm.ua"
label="站点User-Agent"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_interval"
label="单位周期(秒)"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问次数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.proxy"
label="代理"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.render"
label="仿真"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn
v-if="props.oper === 'add'"
@click="emit('close')"
>
取消
</VBtn>
<VBtn
v-else
color="error"
@click="deleteSiteInfo"
>
删除
</VBtn>
<VSpacer />
<VBtn
v-if="props.oper === 'add'"
color="primary"
variant="tonal"
@click="addSite"
>
新增
</VBtn>
<VBtn
v-else
color="primary"
variant="tonal"
@click="updateSiteInfo"
>
保存
</VBtn>
</VCardActions>
</VCard>
</VBottomSheet>
</template>

View File

@@ -0,0 +1,355 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators'
import api from '@/api'
import type { Site, Subscribe } from '@/api/types'
// 输入参数
const props = defineProps({
subid: Number,
})
// 定义触发的自定义事件
const emit = defineEmits(['remove', 'save', 'close'])
// 站点数据列表
const siteList = ref<Site[]>([])
// 站点选择下载框
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
// 订阅编辑表单
const subscribeForm = ref<Subscribe>({
id: props.subid ?? 0,
keyword: '',
quality: '',
resolution: '',
effect: '',
include: '',
exclude: '',
total_episode: 0,
start_episode: 0,
best_version: 0,
sites: [],
type: '',
name: '',
year: '',
tmdbid: 0,
state: '',
last_update: '',
username: '',
current_priority: 0,
})
// 提示框
const $toast = useToast()
// 调用API修改订阅
async function updateSubscribeInfo() {
try {
subscribeForm.value.best_version = subscribeForm.value.best_version ? 1 : 0
const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm.value)
// 提示
if (result.success) {
$toast.success(`${subscribeForm.value.name} 更新成功!`)
// 通知父组件刷新
emit('save')
}
else { $toast.error(`${subscribeForm.value.name} 更新失败:${result.message}`) }
}
catch (e) {
console.log(e)
}
}
// 获取站点列表数据
async function loadSites() {
try {
const data: Site[] = await api.get('site/rss')
// 过滤站点,只有启用的站点才显示
siteList.value = data.filter(item => item.is_active)
}
catch (error) {
console.error(error)
}
}
// 获取站点列表选择框数据
async function getSiteList() {
// 加载订阅站点列表
if (!siteList.value.length)
await loadSites()
const maps = siteList.value.map((item) => {
return {
title: item.name,
value: item.id,
}
})
selectSitesOptions.value = maps.flat()
}
// 获取订阅信息
async function getSubscribeInfo() {
try {
const result: Subscribe = await api.get(
`subscribe/${props.subid}`,
)
subscribeForm.value = result
subscribeForm.value.best_version = subscribeForm.value.best_version === 1
}
catch (e) {
console.log(e)
}
}
// 删除订阅
async function removeSubscribe() {
try {
const result: { [key: string]: any } = await api.delete(
`subscribe/${props.subid}`,
)
if (result.success) {
// 通知父组件刷新
emit('remove')
}
}
catch (e) {
console.log(e)
}
}
watchEffect(() => {
if (props.subid)
getSubscribeInfo()
})
// 质量选择框数据
const qualityOptions = ref([
{
title: '全部',
value: '',
},
{
title: '蓝光原盘',
value: 'Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|MiniBD',
},
{
title: 'Remux',
value: 'Remux',
},
{
title: 'BluRay',
value: 'Blu-?Ray',
},
{
title: 'UHD',
value: 'UHD|UltraHD',
},
{
title: 'WEB-DL',
value: 'WEB-?DL|WEB-?RIP',
},
{
title: 'HDTV',
value: 'HDTV',
},
{
title: 'H265',
value: '[Hx].?265|HEVC',
},
{
title: 'H264',
value: '[Hx].?264|AVC',
},
])
// 分辨率选择框数据
const resolutionOptions = ref([
{
title: '全部',
value: '',
},
{
title: '4k',
value: '4K|2160p|x2160',
},
{
title: '1080p',
value: '1080[pi]|x1080',
},
{
title: '720p',
value: '720[pi]|x720',
},
])
// 特效选择框数据
const effectOptions = ref([
{
title: '全部',
value: '',
},
{
title: '杜比视界',
value: 'Dolby[\\s.]+Vision|DOVI|[\\s.]+DV[\\s.]+',
},
{
title: '杜比全景声',
value: 'Dolby[\\s.]*\\+?Atmos|Atmos',
},
{
title: 'HDR',
value: '[\\s.]+HDR[\\s.]+|HDR10|HDR10\\+',
},
{
title: 'SDR',
value: '[\\s.]+SDR[\\s.]+',
},
])
// 初始化
onMounted(async () => {
// 加载订阅站点列表
getSiteList()
})
</script>
<template>
<VBottomSheet
inset
scrollable
>
<VCard
:title="`编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`"
class="rounded-t"
>
<VCardText class="pt-2">
<DialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="8"
>
<VTextField
v-model="subscribeForm.keyword"
label="搜索关键词"
/>
</VCol>
<VCol
v-if="subscribeForm.type === '电视剧'"
cols="12"
md="2"
>
<VTextField
v-model="subscribeForm.total_episode"
label="总集数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
v-if="subscribeForm.type === '电视剧'"
cols="12"
md="2"
>
<VTextField
v-model="subscribeForm.start_episode"
label="开始集数"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.quality"
label="质量"
:items="qualityOptions"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.resolution"
label="分辨率"
:items="resolutionOptions"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.effect"
label="特效"
:items="effectOptions"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="subscribeForm.include"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="subscribeForm.exclude"
label="排除(关键字、正则式)"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.sites"
:items="selectSitesOptions"
chips
label="订阅站点"
multiple
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VSwitch
v-model="subscribeForm.best_version"
label="洗版"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn color="error" @click="removeSubscribe">
取消订阅
</VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="updateSubscribeInfo"
>
保存
</VBtn>
</VCardActions>
</VCard>
</VBottomSheet>
</template>

View File

@@ -5,6 +5,7 @@ import { type PropType, ref } from 'vue'
interface RenderProps {
component: string
text: string
html: string
content?: any
props?: any
}
@@ -16,9 +17,10 @@ const elementProps = defineProps({
})
// 配置元素
const formItem = ref<RenderProps>(elementProps.config || {
const formItem = ref<RenderProps>(elementProps.config ?? {
component: 'div',
text: '',
html: '',
props: {},
content: [],
})
@@ -30,6 +32,7 @@ const formData = ref<any>(elementProps.form || {})
<template>
<Component
:is="formItem.component"
v-if="!formItem.html"
v-bind="formItem.props"
v-model="formData[formItem.props?.model || '']"
>
@@ -42,4 +45,10 @@ const formData = ref<any>(elementProps.form || {})
:form="formData"
/>
</Component>
<Component
:is="formItem.component"
v-if="formItem.html"
v-bind="formItem.props"
v-html="formItem.html"
/>
</template>

View File

@@ -13,11 +13,10 @@ interface RenderProps {
// 输入参数
const elementProps = defineProps({
config: Object as PropType<RenderProps>,
handler: Boolean,
})
// 配置元素
const formItem = ref<RenderProps>(elementProps.config || {
const formItem = ref<RenderProps>(elementProps.config ?? {
component: 'div',
text: '',
html: '',

View File

@@ -8,6 +8,9 @@ const searchWord = ref<string>('')
// 搜索弹窗
const searchDialog = ref(false)
// ref
const searchWordInput = ref<HTMLElement | null>(null)
// Search
function search() {
if (!searchWord.value)
@@ -21,6 +24,14 @@ function search() {
},
})
}
// 打开搜索弹窗
function openSearchDialog() {
searchDialog.value = true
nextTick(() => {
searchWordInput.value?.focus()
})
}
</script>
<template>
@@ -34,23 +45,16 @@ function search() {
max-width="50rem"
transition="dialog-top-transition"
>
<!-- Dialog Activator -->
<template #activator="{ props }">
<IconBtn
class="d-lg-none"
v-bind="props"
>
<VIcon icon="mdi-magnify" />
</IconBtn>
</template>
<!-- Dialog Content -->
<VCard title="搜索">
<VCardText>
<VRow>
<VCol cols="12">
<VTextField
ref="searchWordInput"
v-model="searchWord"
label="电影、电视剧名称"
@keydown.enter="search"
/>
</VCol>
</VRow>
@@ -59,8 +63,8 @@ function search() {
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="search"
@keydown.enter="search"
>
搜索
</VBtn>
@@ -68,7 +72,13 @@ function search() {
</VCard>
</VDialog>
</div>
<!-- 👉 Search Icon -->
<IconBtn
class="d-lg-none"
@click="openSearchDialog"
>
<VIcon icon="mdi-magnify" />
</IconBtn>
<!-- 👉 Search Textfield -->
<span class="w-1/5">
<VTextField

View File

@@ -38,6 +38,9 @@ async function restart() {
dialogProps: {
maxWidth: '30rem',
},
cancellationButtonProps: {
variant: 'tonal',
},
})
if (confirmed) {
@@ -170,21 +173,21 @@ const avatar = store.state.auth.avatar
<!-- !SECTION -->
</VAvatar>
<!-- 重启进度框 -->
<vDialog
<VDialog
v-model="progressDialog"
width="25rem"
>
<vCard
<VCard
color="primary"
>
<vCardText class="text-center">
<VCardText class="text-center">
正在重启 ...
<vProgressLinear
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</vCardText>
</vCard>
</vDialog>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -10,7 +10,7 @@ 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-default.css'
import 'vue-toast-notification/dist/theme-bootstrap.css'
import { removeEl } from '@/util'
loadFonts()

View File

@@ -28,6 +28,12 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
title="热门电视剧"
/>
<MediaCardSlideView
apipath="douban/tv_animation"
linkurl="/browse/douban/tv_animation?title=热门动漫"
title="热门动漫"
/>
<MediaCardSlideView
apipath="douban/movies"
linkurl="/browse/douban/movies?title=最新电影"

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import api from '@/api'
import type { Context } from '@/api/types'
import store from '@/store'
import TorrentCardListView from '@/views/discover/TorrentCardListView.vue'
import TorrentRowListView from '@/views/discover/TorrentRowListView.vue'
// 路由参数
const route = useRoute()
@@ -12,14 +16,128 @@ const type = route.query?.type?.toString() ?? ''
// 搜索字段
const area = route.query?.area?.toString() ?? ''
// 视图类型从localStorage中读取
const viewType = ref<string>(localStorage.getItem('MPTorrentsViewType') ?? 'card')
// 数据列表
const dataList = ref<Array<Context>>([])
// 是否刷新过
const isRefreshed = ref(false)
// 加载进度文本
const progressText = ref('')
// 加载进度
const progressValue = ref(0)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '正在搜索,请稍候...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
)
progressEventSource.value.onmessage = (event) => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
}
}
}
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
// 设置视图类型
function setViewType(type: string) {
localStorage.setItem('MPTorrentsViewType', type)
viewType.value = type
}
// 获取搜索列表数据
async function fetchData() {
try {
if (!keyword) {
// 查询上次搜索结果
dataList.value = await api.get('search/last')
}
else {
startLoadingProgress()
// 优先按TMDBID精确查询
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:')) {
dataList.value = await api.get(`search/media/${keyword}`, {
params: {
mtype: type,
area,
},
})
}
else {
// 按标题模糊查询
dataList.value = await api.get(`search/title/${keyword}`)
}
stopLoadingProgress()
}
// 标记已刷新
isRefreshed.value = true
}
catch (error) {
console.error(error)
return Promise.reject(error)
}
}
// 加载数据
onMounted(() => {
fetchData()
})
</script>
<template>
<div>
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center">
<VProgressCircular v-if="!keyword" size="48" indeterminate color="primary" />
<VProgressCircular v-if="keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" />
<span>{{ progressText }}</span>
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有资源"
error-description="没有搜索到符合条件的资源"
/>
<div v-if="dataList.length > 0">
<TorrentRowListView
v-if="viewType === 'list'"
:items="dataList"
/>
<TorrentCardListView
:keyword="keyword"
:type="type"
:area="area"
v-else
:items="dataList"
/>
</div>
<!-- 视图切换 -->
<span v-if="dataList.length > 0" class="fixed right-5 bottom-5">
<VBtn
v-if="viewType === 'list'"
icon="mdi-view-grid"
color="primary"
@click="setViewType('card')"
/>
<VBtn
v-else
icon="mdi-view-list"
color="primary"
@click="setViewType('list')"
/>
</span>
</template>

View File

@@ -2,12 +2,12 @@
import { useRoute } from 'vue-router'
import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue'
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
const route = useRoute()
@@ -36,9 +36,9 @@ const tabs = [
tab: 'subscribe',
},
{
title: '规则',
icon: 'mdi-filter-cog',
tab: 'filter',
title: '服务',
icon: 'mdi-list-box',
tab: 'service',
},
{
title: '通知',
@@ -60,7 +60,10 @@ const tabs = [
<template>
<div>
<VTabs v-model="activeTab" show-arrows>
<VTabs
v-model="activeTab"
show-arrows
>
<VTab v-for="item in tabs" :key="item.icon" :value="item.tab">
<VIcon size="20" start :icon="item.icon" />
{{ item.title }}
@@ -68,15 +71,19 @@ const tabs = [
</VTabs>
<VDivider />
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition">
<!-- Account -->
<VWindow
v-model="activeTab"
class="mt-5 disable-tab-transition"
:touch="false"
>
<!-- 用户 -->
<VWindowItem value="account">
<transition name="fade-slide" appear>
<AccountSettingAccount />
</transition>
</VWindowItem>
<!-- 用户 -->
<!-- 站点 -->
<VWindowItem value="site">
<transition name="fade-slide" appear>
<AccountSettingSite />
@@ -97,10 +104,10 @@ const tabs = [
</transition>
</VWindowItem>
<!-- Notification -->
<VWindowItem value="filter">
<!-- 服务 -->
<VWindowItem value="service">
<transition name="fade-slide" appear>
<AccountSettingRule />
<AccountSettingService />
</transition>
</VWindowItem>

View File

@@ -103,6 +103,7 @@ const router = createRouter({
component: () => import('../pages/browse.vue'),
props: true,
meta: {
keepAlive: true,
requiresAuth: true,
},
},
@@ -111,6 +112,7 @@ const router = createRouter({
component: () => import('../pages/credits.vue'),
props: true,
meta: {
keepAlive: true,
requiresAuth: true,
},
},

View File

@@ -8,6 +8,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { formatSeason } from '@/@core/utils/formatters'
import router from '@/router'
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue'
// 输入参数
const mediaProps = defineProps({
@@ -21,6 +22,9 @@ const $toast = useToast()
// 媒体详情
const mediaDetail = ref<MediaInfo>({} as MediaInfo)
// 订阅编辑弹窗
const subscribeEditDialog = ref(false)
// 本地是否存在
const isExists = ref(false)
@@ -39,6 +43,9 @@ const seasonsNotExisted = ref<{ [key: number]: number }>({})
// 各季的订阅状态
const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
// 订阅编号
const subscribeId = ref(0)
// 调用API查询详情
async function getMediaDetail() {
if (mediaProps.mediaid && mediaProps.type) {
@@ -185,7 +192,7 @@ async function addSubscribe(season = 0) {
// 全部存在时洗版
best_version = !seasonsNotExisted.value[season] ? 1 : 0
// 请求API
const result: { [key: string]: any } = await api.post('subscribe', {
const result: { [key: string]: any } = await api.post('subscribe/', {
name: mediaDetail.value?.title,
type: mediaDetail.value?.type,
year: mediaDetail.value?.year,
@@ -211,6 +218,12 @@ async function addSubscribe(season = 0) {
result.message,
best_version,
)
// 显示编辑弹窗
if (result.success) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
}
catch (error) {
console.error(error)
@@ -231,9 +244,7 @@ function showSubscribeAddToast(result: boolean,
if (best_version > 0)
subname = '洗版订阅'
if (result)
$toast.success(`${title} 添加${subname}成功!`)
else
if (!result)
$toast.error(`${title} 添加${subname}失败:${message}`)
}
@@ -684,6 +695,20 @@ onBeforeMount(() => {
error-title="出错啦"
error-description="未识别到TMDB媒体信息"
/>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"
@save="subscribeEditDialog = false"
@remove="() => {
subscribeEditDialog = false;
if (mediaDetail.type === '电影')
checkMovieSubscribed()
else
checkSeasonsSubscribed();
}"
/>
</template>
<style lang="scss">

View File

@@ -1,66 +1,33 @@
<script lang="ts" setup>
import { ref } from 'vue'
import _ from 'lodash'
import api from '@/api'
import type { Context } from '@/api/types'
import TorrentCard from '@/components/cards/TorrentCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import store from '@/store'
// 定义输入参数
const props = defineProps({
// 关键字或TMDBID
keyword: String,
// 类型
type: String,
// 搜索字段
area: String,
})
interface SearchTorrent extends Context {
more?: Array<Context>
}
// 数据列表
const dataList = ref <Array<SearchTorrent>>([])
// 分组后的数据列表
const groupedDataList = ref<Map<string, Context[]>>()
// 是否刷新过
const isRefreshed = ref(false)
// 加载进度文本
const progressText = ref('')
// 加载进度
const progressValue = ref(0)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 定义输入参
const props = defineProps({
// 数据列表
items: Array as PropType<SearchTorrent[]>,
})
// 过滤表单
const filterForm = reactive({
// 站点
site: [] as string[],
// 季
season: [] as string[],
// 制作组
releaseGroup: [] as string[],
// 视频编码
videoCode: [] as string[],
// 促销状态
freeState: [] as string[],
// 质量
edition: [] as string[],
// 分辨率
resolution: [] as string[],
})
@@ -80,110 +47,13 @@ const editionFilterOptions = ref<Array<string>>([])
// 获取分辨率过滤选项
const resolutionFilterOptions = ref<Array<string>>([])
// 按过滤项过滤卡片
watchEffect(() => {
// 清空数据
dataList.value.splice(0)
// 数据列表
const dataList = ref <Array<SearchTorrent>>([])
const match = (filter: Array<string>, value: string | undefined) =>
filter.length === 0 || (value && filter.includes(value))
groupedDataList.value?.forEach((value) => {
if (value.length > 0) {
const matchData = value.filter((data) => {
const { meta_info, torrent_info } = data
// 季、制作组、视频编码
const { season_episode, resource_team, video_encode } = meta_info
return (
// 站点过滤
match(filterForm.site, torrent_info.site_name)
// 促销状态过滤
&& match(filterForm.freeState, torrent_info.volume_factor)
// 季过滤
&& match(filterForm.season, season_episode)
// 制作组过滤
&& match(filterForm.releaseGroup, resource_team)
// 视频编码过滤
&& match(filterForm.videoCode, video_encode)
// 分辨率过滤
&& match(filterForm.resolution, meta_info.resource_pix)
// 质量过滤
&& match(filterForm.edition, meta_info.edition)
)
})
if (matchData.length > 0) {
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
if (matchData.length > 1)
firstData.more = matchData.slice(1)
dataList.value.push(firstData)
}
}
})
})
// 获取订阅列表数据
async function fetchData(): Promise<Array<Context>> {
try {
let searchData: Array<Context>
const keyword = props.keyword ?? ''
const mtype = props.type ?? ''
const area = props.area ?? ''
if (!keyword) {
// 查询上次搜索结果
searchData = await api.get('search/last')
}
else {
startLoadingProgress()
// 优先按TMDBID精确查询
if (props.keyword?.startsWith('tmdb:') || props.keyword?.startsWith('douban:')) {
searchData = await api.get(`search/media/${props.keyword}`, {
params: {
mtype,
area,
},
})
}
else {
// 按标题模糊查询
searchData = await api.get(`search/title/${props.keyword}`)
}
stopLoadingProgress()
}
isRefreshed.value = true
return Promise.resolve(searchData)
}
catch (error) {
console.error(error)
return Promise.reject(error)
}
}
function initData() {
// load data
fetchData().then((data) => {
const groupMap = new Map<string, Context[]>()
data.forEach((item) => {
const { torrent_info } = item
// init options
initOptions(item)
// group data
const key = `${torrent_info.title}_${torrent_info.size}`
if (groupMap.has(key)) {
// 已存在相同标题和大小的分组,将当前上下文信息添加到分组中
const group = groupMap.get(key)
group?.push(item)
}
else {
// 创建新的分组,并将当前上下文信息添加到分组中
groupMap.set(key, [item])
}
})
groupedDataList.value = groupMap
})
}
// 分组后的数据列表
const groupedDataList = ref<Map<string, Context[]>>()
// 初始化过滤选项
function initOptions(data: Context) {
const { torrent_info, meta_info } = data
const optionValue = (options: Array<string>, value: string | undefined) => {
@@ -198,31 +68,69 @@ function initOptions(data: Context) {
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
}
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '正在搜索,请稍候...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
)
progressEventSource.value.onmessage = (event) => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
// 计算分组后的列表
watchEffect(() => {
// 数据分组
const groupMap = new Map<string, Context[]>()
// 遍历数据
props.items?.forEach((item) => {
const { torrent_info } = item
// init options
initOptions(item)
// group data
const key = `${torrent_info.title}_${torrent_info.size}`
if (groupMap.has(key)) {
// 已存在相同标题和大小的分组,将当前上下文信息添加到分组中
const group = groupMap.get(key)
group?.push(item)
}
}
}
else {
// 创建新的分组,并将当前上下文信息添加到分组中
groupMap.set(key, [item])
}
})
groupedDataList.value = groupMap
})
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
// 计算过滤后的列表
watchEffect(() => {
// 清空列表
dataList.value.splice(0)
// 匹配过滤函数
const match = (filter: Array<string>, value: string | undefined) =>
filter.length === 0 || (value && filter.includes(value))
// 加载时获取数据
onMounted(initData)
groupedDataList.value?.forEach((value) => {
if (value.length > 0) {
const matchData = value.filter((data) => {
const { meta_info, torrent_info } = data
// 季、制作组、视频编码
return (
// 站点过滤
match(filterForm.site, torrent_info.site_name)
// 促销状态过滤
&& match(filterForm.freeState, torrent_info.volume_factor)
// 季过滤
&& match(filterForm.season, meta_info.season_episode)
// 制作组过滤
&& match(filterForm.releaseGroup, meta_info.resource_team)
// 视频编码过滤
&& match(filterForm.videoCode, meta_info.video_encode)
// 分辨率过滤
&& match(filterForm.resolution, meta_info.resource_pix)
// 质量过滤
&& match(filterForm.edition, meta_info.edition)
)
})
if (matchData.length > 0) {
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
if (matchData.length > 1)
firstData.more = matchData.slice(1)
dataList.value.push(firstData)
}
}
})
})
</script>
<template>
@@ -307,20 +215,14 @@ onMounted(initData)
</VCol>
</VRow>
</VCard>
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center">
<VProgressCircular v-if="!props.keyword" size="48" indeterminate color="primary" />
<VProgressCircular v-if="props.keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" />
<span>{{ progressText }}</span>
<div class="grid gap-3 grid-torrent-card items-start">
<TorrentCard
v-for="(item, index) in dataList"
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
:torrent="item"
:more="item.more"
/>
</div>
<div v-if="dataList.length > 0" class="grid gap-3 grid-torrent-card items-start">
<TorrentCard v-for="data in dataList" :key="`${data.torrent_info.title}_${data.torrent_info.site_name}_${data.torrent_info.page_url}`" :torrent="data" :more="data.more" />
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有资源"
error-description="没有搜索到符合条件的资源"
/>
</template>
<style lang="scss">

View File

@@ -0,0 +1,245 @@
<script lang="ts" setup>
import type { Context } from '@/api/types'
import TorrentItem from '@/components/cards/TorrentItem.vue'
// 定义输入参数
const props = defineProps({
// 数据列表
items: Array as PropType<Context[]>,
})
// 过滤表单
const filterForm = reactive({
// 站点
site: [] as string[],
// 季
season: [] as string[],
// 制作组
releaseGroup: [] as string[],
// 视频编码
videoCode: [] as string[],
// 促销状态
freeState: [] as string[],
// 质量
edition: [] as string[],
// 分辨率
resolution: [] as string[],
})
// 数据列表
const dataList = ref <Array<Context>>([])
// 获取站点过滤选项
const siteFilterOptions = ref<Array<string>>([])
// 获取季过滤选项
const seasonFilterOptions = ref<Array<string>>([])
// 获取制作组过滤选项
const releaseGroupFilterOptions = ref<Array<string>>([])
// 获取视频编码过滤选项
const videoCodeFilterOptions = ref<Array<string>>([])
// 获取促销状态过滤选项
const freeStateFilterOptions = ref<Array<string>>([])
// 获取质量过滤选项
const editionFilterOptions = ref<Array<string>>([])
// 获取分辨率过滤选项
const resolutionFilterOptions = ref<Array<string>>([])
// 初始化过滤选项
function initOptions(data: Context) {
const { torrent_info, meta_info } = data
const optionValue = (options: Array<string>, value: string | undefined) => {
value && !options.includes(value) && options.push(value)
}
optionValue(siteFilterOptions.value, torrent_info?.site_name)
optionValue(seasonFilterOptions.value, meta_info?.season_episode)
optionValue(releaseGroupFilterOptions.value, meta_info?.resource_team)
optionValue(videoCodeFilterOptions.value, meta_info?.video_encode)
optionValue(freeStateFilterOptions.value, torrent_info?.volume_factor)
optionValue(editionFilterOptions.value, meta_info?.edition)
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
}
// 计算过滤后的列表
watchEffect(() => {
// 清空列表
dataList.value.splice(0)
// 匹配过滤函数
const match = (filter: Array<string>, value: string | undefined) =>
filter.length === 0 || (value && filter.includes(value))
props.items?.forEach((data) => {
const { meta_info, torrent_info } = data
if (
// 站点过滤
match(filterForm.site, torrent_info.site_name)
// 促销状态过滤
&& match(filterForm.freeState, torrent_info.volume_factor)
// 季过滤
&& match(filterForm.season, meta_info.season_episode)
// 制作组过滤
&& match(filterForm.releaseGroup, meta_info.resource_team)
// 视频编码过滤
&& match(filterForm.videoCode, meta_info.video_encode)
// 分辨率过滤
&& match(filterForm.resolution, meta_info.resource_pix)
// 质量过滤
&& match(filterForm.edition, meta_info.edition)
)
dataList.value.push(data)
})
})
// 初始化过滤选项
onMounted(() => {
props.items?.forEach((item) => {
initOptions(item)
})
})
</script>
<template>
<VRow>
<VCol>
<VList
lines="three"
class="rounded"
>
<TorrentItem
v-for="(item, index) in dataList"
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
:torrent="item"
/>
<VListItem v-if="dataList.length === 0">
<VListItemTitle>没有附合当前过滤条件的资源</VListItemTitle>
</VListItem>
</VList>
</VCol>
<VCol
xl="2"
md="3"
class="d-none d-md-block"
>
<VList lines="one" class="rounded">
<VListSubheader v-if="siteFilterOptions.length > 0">
站点
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.site" column multiple>
<VChip
v-for="site in siteFilterOptions"
:key="site"
:color="filterForm.site.includes(site) ? 'primary' : ''"
filter
variant="outlined"
:value="site"
>
{{ site }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="editionFilterOptions.length > 0">
质量
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.edition" column multiple>
<VChip
v-for="edition in editionFilterOptions"
:key="edition"
:color="filterForm.edition.includes(edition) ? 'primary' : ''"
filter
variant="outlined"
:value="edition"
>
{{ edition }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="resolutionFilterOptions.length > 0">
分辨率
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.resolution" column multiple>
<VChip
v-for="resolution in resolutionFilterOptions"
:key="resolution"
:color="filterForm.resolution.includes(resolution) ? 'primary' : ''"
filter
variant="outlined"
:value="resolution"
>
{{ resolution }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="releaseGroupFilterOptions.length > 0">
制作组
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.releaseGroup" column multiple>
<VChip
v-for="releaseGroup in releaseGroupFilterOptions"
:key="releaseGroup"
:color="filterForm.releaseGroup.includes(releaseGroup) ? 'primary' : ''"
filter
variant="outlined"
:value="releaseGroup"
>
{{ releaseGroup }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="videoCodeFilterOptions.length > 0">
视频编码
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.videoCode" column multiple>
<VChip
v-for="videoCode in videoCodeFilterOptions"
:key="videoCode"
:color="filterForm.videoCode.includes(videoCode) ? 'primary' : ''"
filter
variant="outlined"
:value="videoCode"
>
{{ videoCode }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="freeStateFilterOptions.length > 0">
促销状态
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.freeState" column multiple>
<VChip
v-for="freeState in freeStateFilterOptions"
:key="freeState"
:color="filterForm.freeState.includes(freeState) ? 'primary' : ''"
filter
variant="outlined"
:value="freeState"
>
{{ freeState }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="seasonFilterOptions.length > 0">
季集
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.season" column multiple>
<VChip
v-for="season in seasonFilterOptions"
:key="season"
:color="filterForm.season.includes(season) ? 'primary' : ''"
filter
variant="outlined"
:value="season"
>
{{ season }}
</VChip>
</VChipGroup>
</VListItem>
</VList>
</VCol>
</VRow>
</template>

View File

@@ -38,7 +38,7 @@ function pluginInstalled() {
// 获取插件列表数据
async function fetchData() {
try {
dataList.value = await api.get('plugin')
dataList.value = await api.get('plugin/')
isRefreshed.value = true
}
catch (error) {

View File

@@ -17,7 +17,7 @@ const isRefreshed = ref(false)
// 获取订阅列表数据
async function fetchData() {
try {
dataList.value = await api.get('download')
dataList.value = await api.get('download/')
isRefreshed.value = true
}
catch (error) {

View File

@@ -1,10 +1,9 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useToast } from 'vue-toast-notification'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api'
import type { TransferHistory } from '@/api/types'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
import ReorganizeForm from '@/components/form/ReorganizeForm.vue'
// 提示框
const $toast = useToast()
@@ -12,21 +11,15 @@ const $toast = useToast()
// 重新整理对话框
const redoDialog = ref(false)
// TMDB编号
const redoTmdbId = ref('')
// 类型
const redoType = ref('电影')
// 类型下拉框:电影、电视剧
const redoTypeItems = ref([
{ title: '电影', value: '电影' },
{ title: '电视剧', value: '电视剧' },
])
// 当前操作记录
const currentHistory = ref<TransferHistory>()
// 重新整理IDS
const redoIds = ref<number[]>([])
// 重新整理target
const redoTarget = ref('')
// 已选中的数据
const selected = ref<TransferHistory[]>([])
@@ -68,9 +61,6 @@ const progressText = ref('请稍候 ...')
// 进度值
const progressValue = ref(0)
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 删除确认对话框
const deleteConfirmDialog = ref(false)
@@ -127,12 +117,14 @@ const TransferDict: { [key: string]: string } = {
move: '移动',
link: '硬链接',
softlink: '软链接',
rclone_copy: 'Rclone复制',
rclone_move: 'Rclone移动',
}
// 删除历史记录
async function removeHistory(item: TransferHistory) {
currentHistory.value = item
confirmTitle.value = `确认删除 ${item.title} ?`
confirmTitle.value = `确认删除 ${item.title} ${item.seasons}${item.episodes} ?`
deleteConfirmDialog.value = true
}
@@ -211,49 +203,28 @@ async function deleteConfirmHandler(deleteSrc: boolean, deleteDest: boolean) {
async function removeHistoryBatch() {
if (selected.value.length === 0)
return
// 清空当前操作记录
currentHistory.value = undefined
confirmTitle.value = `确认删除 ${selected.value.length} 条记录 ?`
// 打开确认弹窗
deleteConfirmDialog.value = true
}
// 重新整理
async function rehandleHistory() {
try {
if (!redoTmdbId.value || !redoType.value)
return
redoDialog.value = false
$toast.info(`正在重新整理 ${currentHistory.value?.title} ...`)
// 调用API接口重新转移
const requestData = {
...currentHistory.value,
}
const result: { [key: string]: any } = await api.post(
'history/transfer',
requestData,
{
params: {
mtype: redoType.value,
new_tmdbid: parseInt(redoTmdbId.value),
},
},
)
if (result.success) {
fetchData({
page: currentPage.value,
itemsPerPage: itemsPerPage.value,
})
}
else {
$toast.error(`重新整理失败: ${result.message}`)
}
}
catch (e) {
console.log(e)
}
// 批量重新整理
async function retransferBatch() {
if (selected.value.length === 0)
return
// 清空当前操作记录
currentHistory.value = undefined
// 重新整理IDS
redoIds.value = selected.value.map(item => item.id)
// 重新整理target
if (selected.value.length === 1)
redoTarget.value = selected.value[0].dest ?? ''
else
redoTarget.value = ''
// 打开识别弹窗
redoDialog.value = true
}
// 弹出菜单
@@ -264,8 +235,9 @@ const dropdownItems = ref([
props: {
prependIcon: 'mdi-redo-variant',
click: (item: TransferHistory) => {
redoIds.value = [item.id]
redoTarget.value = item.dest ?? ''
redoDialog.value = true
currentHistory.value = item
},
},
},
@@ -392,46 +364,18 @@ const dropdownItems = ref([
</template>
</VDataTableServer>
</VCard>
<VDialog
v-model="redoDialog"
max-width="50rem"
<!-- 底部操作按钮 -->
<span
v-if="selected.length > 0"
class="fixed right-5 bottom-5"
>
<!-- Dialog Content -->
<VCard title="重新整理">
<VCardText>
<VRow>
<VCol cols="12" md="4">
<VSelect
v-model="redoType"
label="类型"
:rules="[requiredValidator]"
:items="redoTypeItems"
/>
</VCol>
<VCol cols="12" md="8">
<VTextField
v-model="redoTmdbId"
label="TMDB编号"
:rules="[requiredValidator, numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
@click="rehandleHistory"
@keydown.enter="rehandleHistory"
>
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
<VBtn
icon="mdi-redo-variant"
class="me-2"
color="primary"
size="x-large"
@click="retransferBatch"
/>
<VBtn
icon="mdi-trash-can-outline"
color="error"
@@ -439,43 +383,16 @@ const dropdownItems = ref([
@click="removeHistoryBatch"
/>
</span>
<!-- 进度框 -->
<vDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<vCard
color="primary"
>
<vCardText class="text-center">
{{ progressText }}
<vProgressLinear
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</vCardText>
</vCard>
</vDialog>
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="600"
scrollable
>
<TmdbSelectorCard
v-model="redoTmdbId"
@close="tmdbSelectorDialog = false"
/>
</vDialog>
<!-- 底部弹窗 -->
<VBottomSheet v-model="deleteConfirmDialog" inset>
<VCard :title="confirmTitle" class="text-center">
<VCard class="text-center rounded-t">
<DialogCloseBtn @click="deleteConfirmDialog = false" />
<div class="d-flex flex-column flex-md-row justify-center mb-3">
<VCardTitle class="pe-10">
{{ confirmTitle }}
</VCardTitle>
<div class="d-flex flex-column flex-lg-row justify-center my-3">
<VBtn
color="info"
color="primary"
class="mb-2 mx-2"
@click="deleteConfirmHandler(false, false)"
>
@@ -484,6 +401,13 @@ const dropdownItems = ref([
<VBtn
color="warning"
class="mb-2 mx-2"
@click="deleteConfirmHandler(true, false)"
>
删除历史记录和源文件
</VBtn>
<VBtn
color="info"
class="mb-2 mx-2"
@click="deleteConfirmHandler(false, true)"
>
删除历史记录和媒体库文件
@@ -498,6 +422,24 @@ const dropdownItems = ref([
</div>
</VCard>
</VBottomSheet>
<!-- 文件整理弹窗 -->
<ReorganizeForm
v-model="redoDialog"
:logids="redoIds"
:target="redoTarget"
@done="() => {
redoDialog = false
// 清空当前操作记录
currentHistory = undefined
selected = []
// 刷新
fetchData({
page: currentPage,
itemsPerPage,
})
}"
@close="redoDialog = false"
/>
</template>
<style lang="scss">

View File

@@ -86,7 +86,7 @@ async function saveAccountInfo() {
accountInfo.value.password = newPassword.value
}
try {
const result: { [key: string]: any } = await api.put('user', accountInfo.value)
const result: { [key: string]: any } = await api.put('user/', accountInfo.value)
if (result.success)
$toast.success('用户信息保存成功!')
else
@@ -100,7 +100,7 @@ async function saveAccountInfo() {
// 调用API查询所有用户
async function loadAllUsers() {
try {
const result: User[] = await api.get('/user')
const result: User[] = await api.get('/user/')
allUsers.value = result
}
@@ -131,7 +131,7 @@ async function deactivateUser(user: User) {
try {
user.is_active = !user.is_active
const result: { [key: string]: any } = await api.put('user', user)
const result: { [key: string]: any } = await api.put('user/', user)
if (result.success) {
$toast.success('用户冻结成功!')
loadAllUsers()

View File

@@ -64,6 +64,9 @@ onMounted(() => {
<th scope="col">
Slack
</th>
<th scope="col">
SynologyChat
</th>
</tr>
</thead>
<tbody>
@@ -83,6 +86,9 @@ onMounted(() => {
<td>
<VCheckbox v-model="message.slack" />
</td>
<td>
<VCheckbox v-model="message.synologychat" />
</td>
</tr>
<tr v-if="messagemTypes.length === 0">
<td

View File

@@ -1,163 +0,0 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
// 提示框
const $toast = useToast()
// 种子优先规则
const selectedTorrentPriority = ref<string>('seeder')
// 种子优先规则下拉框
const TorrentPriorityItems = [
{ title: '站点优先', value: 'site' },
{ title: '做种数优先', value: 'seeder' },
]
// 包含与排除规则
const defaultIncludeExcludeFilter = ref({
include: '',
exclude: '',
})
// 查询种子优先规则
async function queryTorrentPriority() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/TorrentsPriority',
)
selectedTorrentPriority.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 查询包含与排除规则
async function queryIncludeExcludeFilter() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultIncludeExcludeFilter',
)
if (result.data?.value)
defaultIncludeExcludeFilter.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存种子优先规则
async function saveTorrentPriority() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/TorrentsPriority',
selectedTorrentPriority.value,
)
if (result.success)
$toast.success('优先规则保存成功')
else
$toast.error('优先规则保存失败!')
}
catch (error) {
console.log(error)
}
}
// 保存包含与排除规则
async function saveIncludeExcludeFilter() {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/DefaultIncludeExcludeFilter',
defaultIncludeExcludeFilter.value,
)
if (result.success)
$toast.success('默认包含/排除规则保存成功')
else
$toast.error('默认包含/排除规则保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
queryTorrentPriority()
queryIncludeExcludeFilter()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="下载优先规则">
<VCardSubtitle> 按站点优先级或资源种子数量排序和择优下载 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="selectedTorrentPriority"
:items="TorrentPriorityItems"
label="优先规则"
outlined
/>
</VCol>
</VRow>
</vform>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveTorrentPriority"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="默认过滤规则">
<VCardSubtitle> 设置在搜索和订阅时默认使用的过滤规则 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="defaultIncludeExcludeFilter.include"
type="text"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultIncludeExcludeFilter.exclude"
type="text"
label="排除(关键字、正则式)"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveIncludeExcludeFilter"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
</template>
<style lang="scss">
.grid-filterrule-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
padding-block-end: 1rem;
}
</style>

View File

@@ -24,6 +24,12 @@ const allSites = ref<Site[]>([])
// 选中订阅站点
const selectedSites = ref<number[]>([])
// 包含与排除规则
const defaultFilterRules = ref({
include: '',
exclude: '',
})
// 查询已设置优先级规则
async function queryCustomFilters() {
try {
@@ -109,7 +115,7 @@ function addFilterCard() {
// 查询所有站点
async function querySites() {
try {
const data: Site[] = await api.get('site')
const data: Site[] = await api.get('site/')
// 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active)
@@ -148,9 +154,83 @@ async function saveSelectedSites() {
}
}
// 上调优先级
function onLevelUp(pri: string) {
// 找到当前卡片
const card = filterCards.value.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的上一张卡片
const prevCard = filterCards.value.find(card => card.pri === (parseInt(pri) - 1).toString())
if (!prevCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = prevCard.pri
prevCard.pri = temp
// 卡片重新按优先级排序
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 下调优先级
function onLevelDown(pri: string) {
// 找到当前卡片
const card = filterCards.value.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的下一张卡片
const nextCard = filterCards.value.find(card => card.pri === (parseInt(pri) + 1).toString())
if (!nextCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = nextCard.pri
nextCard.pri = temp
// 卡片重新按优先级排序
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 查询包含与排除规则
async function queryDefaultFilter() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultSearchFilterRules',
)
if (result.data?.value)
defaultFilterRules.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存包含与排除规则
async function saveDefaultFilter() {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/DefaultSearchFilterRules',
defaultFilterRules.value,
)
if (result.success)
$toast.success('默认包含/排除规则保存成功')
else
$toast.error('默认包含/排除规则保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
queryCustomFilters()
querySites()
queryDefaultFilter()
})
</script>
@@ -191,9 +271,12 @@ onMounted(() => {
v-for="(card, index) in filterCards"
:key="index"
:pri="card.pri"
:maxpri="filterCards.length.toString()"
:rules="card.rules"
@changed="updateFilterCardValue"
@close="filterCardClose(card.pri)"
@leveldown="onLevelDown"
@levelup="onLevelUp"
/>
</div>
</VCardItem>
@@ -215,6 +298,39 @@ onMounted(() => {
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="默认过滤规则">
<VCardSubtitle> 设置在搜索时默认使用的过滤规则 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.include"
type="text"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.exclude"
type="text"
label="排除(关键字、正则式)"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveDefaultFilter"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
</template>

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { ScheduleInfo } from '@/api/types'
// 提示框
const $toast = useToast()
// 定时服务列表
const schedulerList = ref<ScheduleInfo[]>([])
// 定时器
let refreshTimer: NodeJS.Timer | null = null
// 调用API加载定时服务列表
async function loadSchedulerList() {
try {
const res: ScheduleInfo[] = await api.get('dashboard/schedule')
schedulerList.value = res
}
catch (e) {
console.log(e)
}
}
// 任务状态颜色
function getSchedulerColor(status: string) {
switch (status) {
case '正在运行':
return 'success'
case '已停止':
return 'error'
case '等待':
return ''
default:
return ''
}
}
// 执行命令
function runCommand(id: string) {
try {
// 异步提交
api.get('system/runscheduler', {
params: {
jobid: id,
},
})
$toast.success('定时作业执行请求提交成功!')
// 1秒后刷新数据
setTimeout(() => {
loadSchedulerList()
}, 1000)
}
catch (e) {
console.log(e)
}
}
onMounted(() => {
loadSchedulerList()
// 启动定时器
refreshTimer = setInterval(() => {
loadSchedulerList()
}, 5000)
})
// 组件卸载时停止定时器
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
})
</script>
<template>
<VCard title="定时作业">
<VCardText> 手动执行不会影响作业正常的时间表 </VCardText>
<VTable class="text-no-wrap">
<thead>
<tr>
<th scope="col">
任务名称
</th>
<th scope="col">
任务状态
</th>
<th scope="col">
下一次执行时间
</th>
<th scope="col" />
</tr>
</thead>
<tbody>
<tr
v-for="scheduler in schedulerList"
:key="scheduler.id"
>
<td>
{{ scheduler.name }}
</td>
<td>
<VChip :color="getSchedulerColor(scheduler.status)">
{{ scheduler.status }}
</VChip>
</td>
<td>
{{ scheduler.next_run }}
</td>
<td>
<VBtn
size="small"
:disabled="scheduler.status === '正在运行'"
@click="runCommand(scheduler.id)"
>
<template #prepend>
<VIcon>mdi-play</VIcon>
</template>
执行
</VBtn>
</td>
</tr>
<tr v-if="schedulerList.length === 0">
<td
colspan="4"
class="text-center"
>
没有后台服务
</td>
</tr>
</tbody>
</VTable>
</VCard>
</template>

View File

@@ -14,6 +14,15 @@ const resetSitesText = ref('重置站点数据')
// 站点重置按钮可用状态
const resetSitesDisabled = ref(false)
// 种子优先规则
const selectedTorrentPriority = ref<string>('seeder')
// 种子优先规则下拉框
const TorrentPriorityItems = [
{ title: '站点优先', value: 'site' },
{ title: '做种数优先', value: 'seeder' },
]
// 重置站点
async function resetSites() {
try {
@@ -34,10 +43,74 @@ async function resetSites() {
console.log(error)
}
}
// 查询种子优先规则
async function queryTorrentPriority() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/TorrentsPriority',
)
selectedTorrentPriority.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存种子优先规则
async function saveTorrentPriority() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/TorrentsPriority',
selectedTorrentPriority.value,
)
if (result.success)
$toast.success('优先规则保存成功')
else
$toast.error('优先规则保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
queryTorrentPriority()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="下载优先规则">
<VCardSubtitle> 按站点或做种数量优先下载 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="selectedTorrentPriority"
:items="TorrentPriorityItems"
label="优先规则"
outlined
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveTorrentPriority"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="站点重置">
<VCardText>

View File

@@ -27,6 +27,12 @@ const allSites = ref<Site[]>([])
// 选中订阅站点
const selectedRssSites = ref<number[]>([])
// 包含与排除规则
const defaultFilterRules = ref({
include: '',
exclude: '',
})
// 查询用户选中的订阅站点
async function querySelectedRssSites() {
try {
@@ -57,7 +63,7 @@ async function saveSelectedRssSites() {
// 查询所有站点
async function querySites() {
try {
const data: Site[] = await api.get('site')
const data: Site[] = await api.get('site/')
// 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active)
@@ -165,10 +171,84 @@ function addFilterCard(ruleType: string) {
cards.value.push(newCard)
}
// 上调优先级
function onLevelUp(filterCards: FilterCard[], pri: string) {
// 找到当前卡片
const card = filterCards.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的上一张卡片
const prevCard = filterCards.find(card => card.pri === (parseInt(pri) - 1).toString())
if (!prevCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = prevCard.pri
prevCard.pri = temp
// 卡片重新按优先级排序
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 下调优先级
function onLevelDown(filterCards: FilterCard[], pri: string) {
// 找到当前卡片
const card = filterCards.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的下一张卡片
const nextCard = filterCards.find(card => card.pri === (parseInt(pri) + 1).toString())
if (!nextCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = nextCard.pri
nextCard.pri = temp
// 卡片重新按优先级排序
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 查询包含与排除规则
async function queryDefaultFilter() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultFilterRules',
)
if (result.data?.value)
defaultFilterRules.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存包含与排除规则
async function saveDefaultFilter() {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/DefaultFilterRules',
defaultFilterRules.value,
)
if (result.success)
$toast.success('默认包含/排除规则保存成功')
else
$toast.error('默认包含/排除规则保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
querySites()
queryCustomFilters('SubscribeFilterRules')
queryCustomFilters('BestVersionFilterRules')
queryDefaultFilter()
})
</script>
@@ -209,9 +289,12 @@ onMounted(() => {
v-for="(card, index) in subscribeFilterCards"
:key="index"
:pri="card.pri"
:maxpri="subscribeFilterCards.length.toString()"
:rules="card.rules"
@changed="updateFilterCardValue"
@close="filterCardClose('SubscribeFilterRules', card.pri)"
@leveldown="onLevelDown(subscribeFilterCards, card.pri)"
@levelup="onLevelUp(subscribeFilterCards, card.pri)"
/>
</div>
</VCardItem>
@@ -242,9 +325,12 @@ onMounted(() => {
v-for="(card, index) in bestVersionFilterCards"
:key="index"
:pri="card.pri"
:maxpri="bestVersionFilterCards.length.toString()"
:rules="card.rules"
@changed="updateFilterCardValue2"
@close="filterCardClose('BestVersionFilterRules', card.pri)"
@leveldown="onLevelDown(bestVersionFilterCards, card.pri)"
@levelup="onLevelUp(bestVersionFilterCards, card.pri)"
/>
</div>
</VCardItem>
@@ -266,6 +352,39 @@ onMounted(() => {
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="默认过滤规则">
<VCardSubtitle> 设置在订阅时默认使用的过滤规则 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.include"
type="text"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.exclude"
type="text"
label="排除(关键字、正则式)"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveDefaultFilter"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
</template>

View File

@@ -11,6 +11,9 @@ const customIdentifiers = ref('')
// 自定义制作组
const customReleaseGroups = ref('')
// 自定义占位符
const customization = ref('')
// 文件整理屏蔽词
const transferExcludeWords = ref('')
@@ -42,6 +45,20 @@ 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')
}
catch (error) {
console.log(error)
}
}
// 查询已设置的屏蔽词
async function queryTransferExcludeWords() {
try {
@@ -94,6 +111,25 @@ async function saveCustomReleaseGroups() {
}
}
// 保存自定义占位符
async function saveCustomization() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/Customization',
customization.value.split('\n'),
)
if (result.success)
$toast.success('自定义占位符保存成功')
else
$toast.error('自定义占位符保存失败!')
}
catch (error) {
console.log(error)
}
}
// 保存文件整理屏蔽词
async function saveTransferExcludeWords() {
try {
@@ -116,6 +152,7 @@ async function saveTransferExcludeWords() {
onMounted(() => {
queryCustomIdentifiers()
queryCustomReleaseGroups()
queryCustomization()
queryTransferExcludeWords()
})
</script>
@@ -166,6 +203,26 @@ onMounted(() => {
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="自定义占位符">
<VCardSubtitle> 添加自定义占位符识别正则重命名格式中添加{customization}使用 </VCardSubtitle>
<VCardItem>
<VTextarea
v-model="customization"
auto-grow
placeholder="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义"
/>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
@click="saveCustomization"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="文件整理屏蔽词">
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理 </VCardSubtitle>

View File

@@ -1,14 +1,9 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { Site } from '@/api/types'
import SiteCard from '@/components/cards/SiteCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import { numberValidator, requiredValidator } from '@/@validators'
import { doneNProgress, startNProgress } from '@/api/nprogress'
// 提示框
const $toast = useToast()
import SiteAddEditForm from '@/components/form/SiteAddEditForm.vue'
// 数据列表
const dataList = ref<Site[]>([])
@@ -16,49 +11,13 @@ const dataList = ref<Site[]>([])
// 是否刷新过
const isRefreshed = ref(false)
// 新增按钮文本
const addBtnText = ref('新增站点')
// 新增按钮状态
const addBtnState = ref(false)
// 新增站点对话框
const siteAddDialog = ref(false)
// 状态下拉项
const statusItems = [
{ title: '启用', value: true },
{ title: '停用', value: false },
]
// 生成1到50的优先级下拉框选项
const priorityItems = ref(
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
title: item,
value: item,
})),
)
// 站点编辑表单数据
const siteForm = reactive<Site>({
id: 0,
url: '',
pri: 1,
is_active: true,
cookie: '',
ua: '',
limit_interval: 0,
limit_seconds: 0,
limit_count: 0,
proxy: 0,
render: 0,
name: '',
domain: '',
})
// 获取站点列表数据
async function fetchData() {
try {
dataList.value = await api.get('site')
dataList.value = await api.get('site/')
isRefreshed.value = true
}
catch (error) {
@@ -66,38 +25,6 @@ async function fetchData() {
}
}
// 调用API 新增站点
async function addSite() {
if (!siteForm.url)
return
startNProgress()
addBtnText.value = '新增中...'
addBtnState.value = true
try {
const result: { [key: string]: string } = await api.post('site', siteForm)
if (result.success) {
$toast.success('新增站点成功')
// 刷新数据
fetchData()
}
else { $toast.error(`新增站点失败:${result.message}`) }
siteAddDialog.value = false
}
catch (error) {
console.error(error)
}
doneNProgress()
addBtnText.value = '新增站点'
addBtnState.value = false
}
// 加载时获取数据
onBeforeMount(fetchData)
</script>
@@ -132,150 +59,20 @@ onBeforeMount(fetchData)
error-title="没有站点"
error-description="已添加并支持的站点将会在这里显示"
/>
<!-- Dialog Content -->
<VDialog
<!-- 新增站点按钮 -->
<VBtn
icon="mdi-plus"
size="x-large"
class="fixed right-5 bottom-5"
oper="add"
@click="siteAddDialog = true"
/>
<SiteAddEditForm
v-model="siteAddDialog"
max-width="50rem"
persistent
scrollable
>
<!-- Dialog Activator -->
<template #activator="{ props }">
<VBtn
icon="mdi-plus"
v-bind="props"
size="x-large"
class="fixed right-5 bottom-5"
/>
</template>
<VCard title="新增站点">
<DialogCloseBtn @click="siteAddDialog = false" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="siteForm.url"
label="站点地址"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.pri"
label="优先级"
:items="priorityItems"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.is_active"
:items="statusItems"
label="状态"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"
label="站点Cookie"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="siteForm.ua"
label="站点User-Agent"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_interval"
label="单位周期(秒)"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问次数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.proxy"
label="代理"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.render"
label="仿真"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn
@click="siteAddDialog = false"
>
取消
</VBtn>
<VSpacer />
<VBtn
color="primary"
:disabled="addBtnState"
@click="addSite"
>
{{ addBtnText }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
oper="add"
@save="siteAddDialog = false; fetchData()"
@close="siteAddDialog = false"
/>
</template>
<style lang="scss">

View File

@@ -73,7 +73,7 @@ async function eventsHander(subscribe: Subscribe | Rss) {
async function getSubscribes() {
try {
// 订阅
const subscribes: Subscribe[] = await api.get('subscribe')
const subscribes: Subscribe[] = await api.get('subscribe/')
const subEvents = await Promise.all(
subscribes.map(async sub => eventsHander(sub)),

View File

@@ -19,7 +19,7 @@ const dataList = ref<Subscribe[]>([])
// 获取订阅列表数据
async function fetchData() {
try {
dataList.value = await api.get('subscribe')
dataList.value = await api.get('subscribe/')
isRefreshed.value = true
}
catch (error) {

View File

@@ -12,7 +12,6 @@ module.exports = {
},
plugins: [
require('@tailwindcss/aspect-ratio'),
// ...
],
}

379
yarn.lock
View File

@@ -2409,6 +2409,14 @@
dependencies:
vue-demi ">=0.14.0"
accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
dependencies:
mime-types "~2.1.34"
negotiator "0.6.3"
acorn-jsx@^5.2.0, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -2506,6 +2514,11 @@ array-buffer-byte-length@^1.0.0:
call-bind "^1.0.2"
is-array-buffer "^3.0.1"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
array-includes@^3.1.6:
version "3.1.6"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f"
@@ -2648,6 +2661,24 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
body-parser@1.20.1:
version "1.20.1"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
dependencies:
bytes "3.1.2"
content-type "~1.0.4"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
http-errors "2.0.0"
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.11.0"
raw-body "2.5.1"
type-is "~1.6.18"
unpipe "1.0.0"
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@@ -2717,6 +2748,11 @@ builtins@^5.0.1:
dependencies:
semver "^7.0.0"
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -2764,20 +2800,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449:
version "1.0.30001481"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz#f58a717afe92f9e69d0e35ff64df596bfad93912"
integrity sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==
caniuse-lite@^1.0.30001464:
version "1.0.30001509"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001509.tgz#2b7ad5265392d6d2de25cd8776d1ab3899570d14"
integrity sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA==
caniuse-lite@^1.0.30001503:
version "1.0.30001507"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001507.tgz#fae53f6286e7564783eadea9b447819410a59534"
integrity sha512-SFpUDoSLCaE5XYL2jfqe9ova/pbQHEmbheDf5r4diNwbAgR3qxM9NQtfsiSscjqoya5K7kFcHPUQ+VsUkIJR4A==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001503:
version "1.0.30001546"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz"
integrity sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==
chalk@^2.0.0:
version "2.4.2"
@@ -2951,11 +2977,33 @@ confusing-browser-globals@^1.0.10:
resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81"
integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==
content-disposition@0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
dependencies:
safe-buffer "5.2.1"
content-type@~1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
convert-source-map@^1.7.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
cookie@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
core-js-compat@^3.30.1, core-js-compat@^3.30.2:
version "3.31.0"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.31.0.tgz#4030847c0766cc0e803dcdfb30055d7ef2064bf1"
@@ -3127,7 +3175,14 @@ de-indent@^1.0.2:
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
debug@^3.2.7:
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
debug@^3.0.1, debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@@ -3206,6 +3261,16 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
depd@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
destroy@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@@ -3292,6 +3357,11 @@ domutils@^3.0.1:
domelementtype "^2.3.0"
domhandler "^5.0.3"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
ejs@^3.1.6:
version "3.1.9"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361"
@@ -3314,6 +3384,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@@ -3426,6 +3501,11 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es6-promise@^4.1.1:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
esbuild@^0.17.5:
version "0.17.18"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.18.tgz#f4f8eb6d77384d68cd71c53eb6601c7efe05e746"
@@ -3459,6 +3539,11 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -3900,6 +3985,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
execa@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -3915,6 +4005,52 @@ execa@^5.1.1:
signal-exit "^3.0.3"
strip-final-newline "^2.0.0"
express-http-proxy@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/express-http-proxy/-/express-http-proxy-2.0.0.tgz#c47f4d8b53e8d9c2b9a2f1d158eedf6eb84828a5"
integrity sha512-TXxcPFTWVUMSEmyM6iX2sT/JtmqhqngTq29P+eXTVFdtxZrTmM8THUYK59rUXiln0FfPGvxEpGRnVrgvHksXDw==
dependencies:
debug "^3.0.1"
es6-promise "^4.1.1"
raw-body "^2.3.0"
express@^4.18.2:
version "4.18.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "1.20.1"
content-disposition "0.5.4"
content-type "~1.0.4"
cookie "0.5.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "1.2.0"
fresh "0.5.2"
http-errors "2.0.0"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "2.4.1"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.7"
qs "6.11.0"
range-parser "~1.2.1"
safe-buffer "5.2.1"
send "0.18.0"
serve-static "1.15.0"
setprototypeof "1.2.0"
statuses "2.0.1"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
extract-comments@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/extract-comments/-/extract-comments-1.1.0.tgz#b90bca033a056bd69b8ba1c6b6b120fc2ee95c18"
@@ -4000,6 +4136,19 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
finalhandler@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "2.4.1"
parseurl "~1.3.3"
statuses "2.0.1"
unpipe "~1.0.0"
find-cache-dir@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
@@ -4073,11 +4222,21 @@ form-data@^4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
fraction.js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
fs-extra@^9.0.1:
version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
@@ -4391,11 +4550,29 @@ htmlparser2@^8.0.0, htmlparser2@^8.0.1:
domutils "^3.0.1"
entities "^4.4.0"
http-errors@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
dependencies:
depd "2.0.0"
inherits "2.0.4"
setprototypeof "1.2.0"
statuses "2.0.1"
toidentifier "1.0.1"
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
idb@^7.0.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
@@ -4442,7 +4619,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2:
inherits@2, inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -4461,6 +4638,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5:
has "^1.0.3"
side-channel "^1.0.4"
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
is-alphabetical@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
@@ -5051,6 +5233,11 @@ mdn-data@2.0.30:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
meow@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
@@ -5069,6 +5256,11 @@ meow@^9.0.0:
type-fest "^0.18.0"
yargs-parser "^20.2.3"
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -5079,6 +5271,11 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
micromark@~2.11.0:
version "2.11.4"
resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a"
@@ -5100,13 +5297,18 @@ mime-db@1.52.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@@ -5194,12 +5396,17 @@ mlly@^1.1.1, mlly@^1.2.0:
pkg-types "^1.0.2"
ufo "^1.1.1"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@^2.1.1:
ms@2.1.3, ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -5238,6 +5445,11 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
node-fetch@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
@@ -5365,6 +5577,13 @@ object.values@^1.1.6:
define-properties "^1.1.4"
es-abstract "^1.20.4"
on-finished@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
dependencies:
ee-first "1.1.1"
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -5494,6 +5713,11 @@ parse5@^7.0.0:
dependencies:
entities "^4.4.0"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -5514,6 +5738,11 @@ path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -5929,6 +6158,14 @@ prismjs@^1.29.0:
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
dependencies:
forwarded "0.2.0"
ipaddr.js "1.9.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
@@ -5962,6 +6199,13 @@ purgecss@^5.0.0:
postcss "^8.4.4"
postcss-selector-parser "^6.0.7"
qs@6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
dependencies:
side-channel "^1.0.4"
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@@ -5979,6 +6223,31 @@ randombytes@^2.1.0:
dependencies:
safe-buffer "^5.1.0"
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.4.24"
unpipe "1.0.0"
raw-body@^2.3.0:
version "2.5.2"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.4.24"
unpipe "1.0.0"
read-cache@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
@@ -6161,7 +6430,7 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
safe-buffer@^5.1.0:
safe-buffer@5.2.1, safe-buffer@^5.1.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -6182,6 +6451,11 @@ safe-regex@^2.1.1:
dependencies:
regexp-tree "~0.1.1"
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass@^1.59.3:
version "1.62.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.62.1.tgz#caa8d6bf098935bc92fc73fa169fb3790cacd029"
@@ -6213,6 +6487,25 @@ semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7, semve
dependencies:
lru-cache "^6.0.0"
send@0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
dependencies:
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "2.0.0"
mime "1.6.0"
ms "2.1.3"
on-finished "2.4.1"
range-parser "~1.2.1"
statuses "2.0.1"
serialize-javascript@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
@@ -6220,6 +6513,21 @@ serialize-javascript@^4.0.0:
dependencies:
randombytes "^2.1.0"
serve-static@1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.18.0"
setprototypeof@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -6339,6 +6647,11 @@ stable@^0.1.8:
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
statuses@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
stop-iteration-iterator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4"
@@ -6816,6 +7129,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
toidentifier@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
tr46@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
@@ -6909,6 +7227,14 @@ type-fest@^3.10.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.10.0.tgz#d75f17a22be8816aea6315ab2739fe1c0c211863"
integrity sha512-hmAPf1datm+gt3c2mvu0sJyhFy6lTkIGf0GzyaZWxRLnabQfPUqg6tF95RPg6sLxKI7nFLGdFxBcf2/7+GXI+A==
type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
typed-array-length@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"
@@ -6997,6 +7323,11 @@ universalify@^2.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
unplugin-auto-import@^0.15.1:
version "0.15.3"
resolved "https://registry.yarnpkg.com/unplugin-auto-import/-/unplugin-auto-import-0.15.3.tgz#abf5f1bf42f8c181e9dd1067bd5645aad727df6e"
@@ -7075,6 +7406,11 @@ util-deprecate@^1.0.2:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
v8-compile-cache@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
@@ -7088,6 +7424,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
vite-plugin-pages@^0.29.0:
version "0.29.0"
resolved "https://registry.yarnpkg.com/vite-plugin-pages/-/vite-plugin-pages-0.29.0.tgz#8a7352cbbbc463fd2a725d67e221af8de8992c2f"