Compare commits

...

91 Commits

Author SHA1 Message Date
jxxghp
d2aa5a64aa chore(package): bump version to 2.1.2 2024-12-06 15:23:56 +08:00
jxxghp
2620a55c5a refactor(DirectoryCard): update storage options and labels for clarity 2024-12-06 12:09:24 +08:00
jxxghp
14e33215f8 Merge pull request #261 from InfinityPacer/v2 2024-12-05 23:03:14 +08:00
InfinityPacer
6862c2a744 Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2024-12-05 19:37:44 +08:00
InfinityPacer
fb215e8d87 feat(Subscribe): support update subscription status 2024-12-05 19:36:41 +08:00
InfinityPacer
f52ad2151b style(SubscribeCard): support different styles for subscription states 2024-12-05 19:35:23 +08:00
jxxghp
1a47b7d09d Merge pull request #260 from InfinityPacer/v2 2024-12-03 20:53:27 +08:00
InfinityPacer
f292071a34 fix(saveSiteSetting): update success toast message for consistency 2024-12-03 19:10:01 +08:00
InfinityPacer
dd616d29e8 fix(saveSiteSetting): add error toast for failed settings save 2024-12-03 19:07:58 +08:00
jxxghp
0509f18d66 Merge pull request #257 from Aqr-K/v2-rulessettings 2024-11-29 18:32:28 +08:00
Aqr-K
f59fb119e4 Update ImportCodeDialog.vue 2024-11-29 17:33:00 +08:00
Aqr-K
46127cac1f Update FilterRuleGroupCard.vue 2024-11-29 17:31:35 +08:00
Aqr-K
c1abf76211 Update AccountSettingRule.vue 2024-11-29 17:30:11 +08:00
jxxghp
fe5b45d48d Merge pull request #256 from InfinityPacer/v2 2024-11-29 15:14:49 +08:00
InfinityPacer
10ac1ebf7b Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2024-11-29 13:06:50 +08:00
InfinityPacer
e5d8144510 fix(api): update subscribe endpoint URL to include trailing slash 2024-11-29 13:06:32 +08:00
jxxghp
f9a65fba7a 更新 package.json 2024-11-29 07:09:54 +08:00
jxxghp
9b4138349b Merge pull request #255 from wikrin/manual_transfer 2024-11-28 07:24:19 +08:00
Attente
db9c9db5a9 改进手动整理类型/类别可选逻辑 2024-11-28 05:23:20 +08:00
jxxghp
24e992339f Merge pull request #254 from InfinityPacer/v2 2024-11-27 16:26:12 +08:00
InfinityPacer
f26d1babf7 Merge branch 'v2' of https://github.com/jxxghp/MoviePilot-Frontend into v2 2024-11-27 15:32:30 +08:00
InfinityPacer
de3347cea1 feat(encoding): add detection performance mode 2024-11-27 15:32:09 +08:00
jxxghp
e900fac4bd Merge pull request #253 from Aqr-K/v2-main 2024-11-27 07:02:51 +08:00
jxxghp
396218a467 Merge pull request #251 from wikrin/add 2024-11-27 07:02:20 +08:00
Attente
d3a66ffa8c 去除自动选项
- 仅当`目的路径`为空时,才会设为`自动`
- 已选择`整理方式`后, 再选择配置的`媒体库目录`整理方式不再改变
- `自定义路径`时:
- - `整理方式`为自动时,会修改为`复制`,请注意
- - `整理方式`不为自动时, 不会改变
2024-11-27 05:53:31 +08:00
Aqr-K
1e7ffb4c2e Update main.ts 2024-11-26 22:41:22 +08:00
Aqr-K
3df5d4c690 Update main.ts 2024-11-25 16:42:12 +08:00
Aqr-K
02a8331996 fix: 移除多余import 2024-11-25 16:32:01 +08:00
Aqr-K
a29ad6a091 fix: 调整注册顺序,解决重复注册的警告 2024-11-25 16:30:57 +08:00
Attente
3ef1e65412 手动整理中整理方式增加自动 2024-11-25 13:24:43 +08:00
jxxghp
2deaec1fc6 Merge pull request #250 from InfinityPacer/v2 2024-11-24 17:59:22 +08:00
InfinityPacer
c9b0b23d36 fix(downloader): remove redundant checks and prompts 2024-11-24 17:57:25 +08:00
jxxghp
f06cca4ead Merge pull request #249 from wikrin/v2 2024-11-24 16:58:58 +08:00
Attente
a1990ce3e4 fix(dialog): correct storage option selection in Reorganize Dialog 2024-11-24 16:47:44 +08:00
jxxghp
cbbf023030 更新 package.json 2024-11-24 07:35:25 +08:00
jxxghp
307aa724eb Merge pull request #248 from wikrin/rfc-247 2024-11-23 23:07:13 +08:00
Attente
cd6f37d80f Merge branch 'v2' into rfc-247 2024-11-23 23:05:47 +08:00
jxxghp
b903134770 feat(ReorganizeDialog): 添加文件操作整理方式选项 2024-11-23 23:02:50 +08:00
Attente
11effdd297 Revert "refactor: 优化目标目录下拉框和路径变化监听逻辑"
This reverts commit 01f63a4b6b.
2024-11-23 22:42:48 +08:00
jxxghp
8873d8372d feat(ReorganizeDialog): 添加文件操作整理方式选项 2024-11-23 21:32:27 +08:00
jxxghp
964aa29d12 Merge pull request #245 from wikrin/dev 2024-11-23 19:21:03 +08:00
Attente
b45a3c6539 qb下载器添加用户名输入限制 2024-11-23 17:21:11 +08:00
jxxghp
b72b7ad0fb Merge pull request #246 from DDS-Derek/issue_rfc 2024-11-23 12:38:43 +08:00
DDSRem
0e3106d8c1 chore(issue): add rfc template 2024-11-23 12:34:59 +08:00
jxxghp
71a6626fa9 feat(ReorganizeDialog): 添加按类型和类别分类的选项 2024-11-23 11:21:29 +08:00
jxxghp
68006bac88 chore(yarn.lock): 更新terser到版本5.36.0 2024-11-22 13:02:05 +08:00
jxxghp
34cbcc38a6 feat(NetTestView): 添加对api.github.com和raw.githubusercontent.com的支持 2024-11-22 12:40:05 +08:00
jxxghp
f4daee85c7 Merge pull request #243 from Aqr-K/v2-terser 2024-11-21 18:18:20 +08:00
Aqr-K
dd347039b5 Update vite.config.ts 2024-11-21 16:50:48 +08:00
Aqr-K
0c9367d58a Update package.json 2024-11-21 16:50:18 +08:00
jxxghp
af10c4f1c3 feat(TransferHistoryView): 增加重做目标存储的响应式支持 2024-11-21 16:02:28 +08:00
jxxghp
52fbeda941 Merge pull request #242 from wikrin/v2 2024-11-21 14:00:17 +08:00
jxxghp
ace23af363 chore(package): 更新版本号至2.0.9 2024-11-21 12:14:15 +08:00
jxxghp
a097d89d68 feat(DownloadSettings): 更新下载器设置API,优化下载器加载逻辑 2024-11-21 10:26:12 +08:00
jxxghp
77cb817523 feat(AliyunAuthDialog): 添加配置支持,允许自定义refreshToken和保存设置 2024-11-20 20:25:32 +08:00
jxxghp
c956e271a2 refactor(ReorganizeDialog): 重构目标路径输入组件,移除不必要的目录加载逻辑 2024-11-20 19:24:10 +08:00
jxxghp
6413f30d18 chore(package): 更新版本号至2.0.8 2024-11-20 13:16:16 +08:00
jxxghp
789e748df0 fix(U115AuthDialog): 添加配置支持和自定义Cookie输入 2024-11-20 13:15:31 +08:00
Attente
c89edae375 downloader属性不再为可选, 使组件具有初始值 2024-11-20 11:40:24 +08:00
Attente
f4dca4922b Update SiteAddEditDialog.vue 2024-11-20 10:43:47 +08:00
Attente
73b9ef5ee7 fix(dialog): 站点下载器字段修改默认值 2024-11-20 10:38:04 +08:00
jxxghp
462742961a fix(SubscribeEditDialog): 下载器字段更新默认值 2024-11-20 08:26:07 +08:00
jxxghp
5a647fabfa fix(UserProfile): 添加超级用户条件以控制站点认证对话框的显示 2024-11-20 08:16:27 +08:00
jxxghp
2580ceac20 更新 subscribe.vue 2024-11-19 22:23:43 +08:00
jxxghp
6905391785 fix(dialog): 移除目标列的列数限制以提高灵活性 2024-11-19 21:29:00 +08:00
jxxghp
7406226e68 fix(auth): 初始化认证表单时提供默认值以避免空值 2024-11-19 21:24:53 +08:00
jxxghp
af9ee00ad3 Merge pull request #240 from InfinityPacer/v2
feat(site): update site timeout hint to indicate 0 as unlimited
2024-11-19 18:24:30 +08:00
jxxghp
01f63a4b6b refactor: 优化目标目录下拉框和路径变化监听逻辑 2024-11-19 18:10:03 +08:00
jxxghp
45e48755d3 Merge pull request #241 from wikrin/ReorganizeDialog
不再强制绑定目的路径的配置
2024-11-19 18:01:19 +08:00
Attente
f6c740738f 不再强制绑定目的路径的配置
- resolve jxxghp/MoviePilot#2959
2024-11-19 17:00:52 +08:00
InfinityPacer
29780cd4b7 feat(site): update site timeout hint to indicate 0 as unlimited 2024-11-19 11:11:48 +08:00
jxxghp
a050b7c7d5 feat: 更新版本至2.0.7 2024-11-19 08:45:39 +08:00
jxxghp
1f25387f81 Merge pull request #239 from Aqr-K/v2-subscribe 2024-11-18 21:25:28 +08:00
Aqr-K
36fb7b53ba fix: 使用 SubscribeTvTabs 作为标签页 2024-11-18 21:23:17 +08:00
Aqr-K
354295ffda Merge branch 'jxxghp:v2' into v2-subscribe 2024-11-18 21:21:34 +08:00
jxxghp
4f28018f4f feat(MediaCard): 添加媒体卡详情展示,优化悬停效果 2024-11-17 16:54:40 +08:00
jxxghp
5d37666bea fix https://github.com/jxxghp/MoviePilot/issues/3143 2024-11-17 14:46:54 +08:00
jxxghp
705e81db7f chore(package): 更新版本号至 2.0.6 2024-11-17 14:25:43 +08:00
Aqr-K
57d5859727 feat: 增加 订阅分享 标签页 2024-11-17 05:16:50 +08:00
jxxghp
06387ab33e feat(auth): 优化二维码登录状态处理逻辑并更新二维码组件 2024-11-17 02:07:46 +08:00
jxxghp
5f0c3b3639 chore(package): 更新版本号至 2.0.5 2024-11-16 09:47:27 +08:00
jxxghp
414fb8afd1 feat(subscribe): 添加下载器选项到订阅设置 2024-11-16 08:59:49 +08:00
jxxghp
58fbaaa8f4 Merge pull request #237 from wikrin/downloader 2024-11-16 07:54:24 +08:00
Attente
040790a672 fix 资源搜索下载时设置的下载器不生效的问题 2024-11-16 01:41:53 +08:00
Attente
bf36e39f3b feat(site): 添加站点自定义下载器功能 2024-11-16 00:28:56 +08:00
jxxghp
a780946915 Merge pull request #236 from Ricca111111/mpf 2024-11-15 06:42:56 +08:00
jxxghp
1d537c2799 Merge pull request #235 from Aqr-K/v2-settings-rule 2024-11-15 06:41:10 +08:00
Ricca
6a3e383f30 modify FilterRuleGroupCard.vue 2024-11-15 01:33:10 +08:00
Aqr-K
cb72c6b586 错误传参 2024-11-14 23:46:26 +08:00
Aqr-K
384e1a63b3 fix(settings): bug
- 移除空值转换
2024-11-14 23:43:25 +08:00
Aqr-K
e6357d0a54 fix(settings): bug 2024-11-14 23:09:19 +08:00
34 changed files with 922 additions and 303 deletions

45
.github/ISSUE_TEMPLATE/rfc.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: 功能提案
description: Request for Comments
title: '[RFC]'
labels: ['RFC']
body:
- type: markdown
attributes:
value: |
一份提案(RFC)定位为 **「在某功能/重构的具体开发前,用于开发者间 review 技术设计/方案的文档」**
目的是让协作的开发者间清晰的知道「要做什么」和「具体会怎么做」,以及所有的开发者都能公开透明的参与讨论;
以便评估和讨论产生的影响 (遗漏的考虑、向后兼容性、与现有功能的冲突)
因此提案侧重在对解决问题的 **方案、设计、步骤** 的描述上。
如果仅希望讨论是否添加或改进某功能本身,请使用 -> [Issue: 功能改进](https://github.com/jxxghp/MoviePilot/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.yml&title=%5BFeature+Request%5D%3A+)
- type: textarea
id: background
attributes:
label: 背景 or 问题
description: 简单描述遇到的什么问题或需要改动什么。可以引用其他 issue、讨论、文档等。
validations:
required: true
- type: textarea
id: goal
attributes:
label: '目标 & 方案简述'
description: 简单描述提案此提案实现后,**预期的目标效果**,以及简单大致描述会采取的方案/步骤,可能会/不会产生什么影响。
validations:
required: true
- type: textarea
id: design
attributes:
label: '方案设计 & 实现步骤'
description: |
详细描述你设计的具体方案,可以考虑拆分列表或要点,一步步描述具体打算如何实现的步骤和相关细节。
这部份不需要一次性写完整,即使在创建完此提案 issue 后,依旧可以再次编辑修改。
validations:
required: false
- type: textarea
id: alternative
attributes:
label: '替代方案 & 对比'
description: |
[可选] 为来实现目标效果,还考虑过什么其他方案,有什么对比?
validations:
required: false

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.0.4",
"version": "2.1.2",
"private": true,
"bin": "dist/service.js",
"scripts": {
@@ -85,6 +85,7 @@
"stylelint-config-idiomatic-order": "10.0.0",
"stylelint-config-standard-scss": "13.1.0",
"stylelint-use-logical-spec": "5.0.1",
"terser": "^5.36.0",
"type-fest": "^4.15.0",
"typescript": "^5.0.4",
"unplugin-auto-import": "^0.17.5",

View File

@@ -71,3 +71,10 @@ export const storageDict = storageOptions.reduce((dict, item) => {
dict[item.value] = item.title
return dict
}, {} as Record<string, string>)
export const transferTypeOptions = [
{ title: '复制', value: 'copy' },
{ title: '移动', value: 'move' },
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
]

View File

@@ -44,7 +44,7 @@ export interface Subscribe {
lack_episode?: number
// 附加信息
note?: string
// 状态N-新建 R-订阅中
// 状态N-新建 R-订阅中 P-待定 S-暂停
state: string
// 最后更新时间
last_update: string
@@ -72,6 +72,8 @@ export interface Subscribe {
media_category?: string
// 过滤规则组
filter_groups?: string[]
// 下载器
downloader: string
}
// 订阅分享
@@ -387,6 +389,8 @@ export interface Site {
pri?: number
// RSS地址
rss?: string
// 下载器
downloader: string
// Cookie
cookie?: string
// ApiKey
@@ -605,6 +609,8 @@ export interface TorrentInfo {
site_proxy: boolean
// 站点优先级
site_order: number
// 站点下载器
site_downloader?: string
// 种子名称
title?: string
// 种子副标题
@@ -1045,7 +1051,7 @@ export interface TransferDirectoryConf {
// 监控模式 fast/compatibility
monitor_mode?: string
// 整理方式 move/copy/link/softlink
transfer_type?: string
transfer_type: string
// 文件覆盖模式 always/size/never/latest
overwrite_mode?: string
// 整理到媒体库目录
@@ -1133,3 +1139,42 @@ export interface SubscrbieInfo {
// 集信息 {集号: {download: 文件路径library: 文件路径, backdrop: url, title: 标题, description: 描述}}
episodes: Record<number, SubscribeEpisodeInfo>
}
export interface TransferForm {
// 文件项
fileitem: FileItem
// 历史ID
logid: number
// 目标存储
target_storage: string
// 目标路径
target_path: string
// TMDB ID
tmdbid?: number
// 豆瓣 ID
doubanid?: string
// 季号
season?: number
// 类型
type_name?: string
// 整理方式
transfer_type: string
// 自定义格式
episode_format?: string
// 指定集数
episode_detail?: string
// 指定PART
episode_part?: string
// 集数偏移
episode_offset?: string
// 最小文件大小
min_filesize: number
// 刮削
scrape: boolean
// 复用历史识别信息
from_history: boolean
// 媒体库类型子目录
library_type_folder?: boolean
// 媒体库类别子目录
library_category_folder?: boolean
}

View File

@@ -220,7 +220,12 @@ watch(
/>
</VCol>
<VCol cols="4">
<VSelect v-model="props.directory.storage" variant="underlined" :items="storageOptions" label="下载存储" />
<VSelect
v-model="props.directory.storage"
variant="underlined"
:items="storageOptions"
label="下载存储/源存储"
/>
</VCol>
<VCol cols="8">
<VPathField @update:modelValue="updateDownloadPath" :storage="props.directory.storage">
@@ -229,7 +234,7 @@ watch(
v-model="props.directory.download_path"
v-bind="menuprops"
variant="underlined"
label="下载目录"
label="下载目录/源目录"
/>
</template>
</VPathField>

View File

@@ -6,7 +6,7 @@ import { useToast } from 'vue-toast-notification'
import type { DownloaderInfo } from '@/api/types'
import qbittorrent_image from '@images/logos/qbittorrent.png'
import transmission_image from '@images/logos/transmission.png'
import {cloneDeep} from "lodash";
import { cloneDeep } from 'lodash'
// 定义输入
const props = defineProps({
@@ -211,7 +211,6 @@ onUnmounted(() => {
<VTextField
v-model="downloaderInfo.config.username"
label="用户名"
placeholder="admin"
hint="登录使用的用户名"
persistent-hint
active
@@ -289,7 +288,6 @@ onUnmounted(() => {
<VTextField
v-model="downloaderInfo.config.username"
label="用户名"
placeholder="admin"
hint="登录使用的用户名"
persistent-hint
active

View File

@@ -48,10 +48,10 @@ const groupInfoDialog = ref(false)
// 规则详情
const groupInfo = ref<FilterRuleGroup>({
name: props.group?.name,
rule_string: props.group?.rule_string,
media_type: props.group?.media_type,
category: props.group?.category,
name: props.group?.name ?? '',
rule_string: props.group?.rule_string ?? '',
media_type: props.group?.media_type ?? '',
category: props.group?.category ?? '',
})
// 媒体类型字典
@@ -64,24 +64,30 @@ const mediaTypeItems = [
// 根据选中的媒体类型,获取对应的媒体类别
const getCategories = computed(() => {
const default_value = [{ title: '全部', value: '' }]
if (!props.categories || !groupInfo.value.media_type || !props.categories[groupInfo.value.media_type ?? ''])
if (!props.categories || !groupInfo.value.media_type || !props.categories[groupInfo.value.media_type]) {
return default_value
return default_value.concat(props.categories[groupInfo.value.media_type ?? ''])
}
return default_value.concat(props.categories[groupInfo.value.media_type] || [])
})
// 规则组规则卡片列表
const filterRuleCards = ref<FilterCard[]>([])
// 规则组类型,仅用于导入判断
const filterRuleCardsType = ref<FilterCard>({
pri: '',
rules: [],
})
// 导入代码弹窗
const importCodeDialog = ref(false)
// 导入代码
const importCodeString = ref('')
// 导入代码类型
const importCodeType = ref('')
// 更新规则卡片的值
function updateFilterCardValue(pri: string, rules: string[]) {
const card = filterRuleCards.value.find(card => card.pri === pri)
if (card) card.rules = rules
if (card && Array.isArray(rules)) card.rules = rules
}
// 移除卡片
@@ -96,98 +102,90 @@ function filterCardClose(pri: string) {
// 分享规则
function shareRules() {
// 有值才处理
if (filterRuleCards.value.length === 0) return
// 将卡片规则接装为字符串
const value = filterRuleCards.value
.filter(card => card.rules.length > 0)
.filter(card => Array.isArray(card.rules) && card.rules.length > 0)
.map(card => card.rules.join('&'))
.join('>')
// 复制到剪贴板
try {
copyToClipboard(value)
$toast.success('优先级规则已复制到剪贴板')
} catch (error) {
$toast.error('优先级规则复制失败!')
console.error(error)
}
}
// 导入规则
async function importRules() {
importCodeString.value = ''
async function importRules(ruleType: string) {
importCodeType.value = ruleType
importCodeDialog.value = true
}
// 监听导入代码变化
watchEffect(() => {
if (!importCodeString.value) return
// 导入代码需要以空格开头和结束,没有则拼接
if (!importCodeString.value.startsWith(' ')) importCodeString.value = ` ${importCodeString.value}`
if (!importCodeString.value.endsWith(' ')) importCodeString.value = `${importCodeString.value} `
// 将导入的代码转换为规则卡片
const groups = importCodeString.value.split('>')
filterRuleCards.value = groups.map((group: string, index: number) => {
return {
pri: (index + 1).toString(),
rules: group.split('&'),
// 保存导入代码,直接覆盖原有值
function saveCodeString(type: string, code: any) {
try {
code = code.value
if (type === 'priority') {
// 解析值
if (!code) return
// 首尾增加空格
if (!code.startsWith(' ')) code = ` ${code}`
if (!code.endsWith(' ')) code = `${code} `
const groups = code.split('>')
filterRuleCards.value = groups.map((group: string, index: number) => ({
pri: (index + 1).toString(),
rules: group.split('&').filter(rule => rule),
}))
}
})
})
} catch (error) {
$toast.error('导入失败!')
console.error(error)
}
}
// 增加卡片
function addFilterCard() {
// 优先级
const pri = (filterRuleCards.value.length + 1).toString()
// 新卡片
const newCard: FilterCard = { pri, rules: [] }
// 添加到列表
filterRuleCards.value.push(newCard)
}
// 根据列表的拖动顺序更新优先级
function dragOrderEnd() {
filterRuleCards.value.map((card, index) => {
filterRuleCards.value.forEach((card, index) => {
card.pri = (index + 1).toString()
return card
})
}
// 打开详情弹窗
function opengroupInfoDialog() {
// 深复制
groupInfo.value = cloneDeep(props.group)
if (props.group.rule_string) {
filterRuleCards.value = props.group.rule_string.split('>').map((group: string, index: number) => {
return {
pri: (index + 1).toString(),
rules: group.split('&'),
}
})
filterRuleCards.value = props.group.rule_string.split('>').map((group: string, index: number) => ({
pri: (index + 1).toString(),
rules: group.split('&').filter(rule => rule),
}))
}
groupInfoDialog.value = true
}
// 保存详情数据
function saveGroupInfo() {
// 为空
if (!groupInfo.value.name) {
if (!groupInfo.value.name.trim()) {
$toast.error('规则组名称不能为空')
return
}
// 重名判断
if (props.groups.some(item => item.name === groupInfo.value.name && item !== props.group)) {
$toast.error(`规则组名称【${groupInfo.value.name}】已存在,请替换`)
return
}
// 保存
groupInfoDialog.value = false
// 更新到 groupInfo的rule_string
groupInfo.value.rule_string = filterRuleCards.value
.filter(card => card.rules.length > 0)
.filter(card => Array.isArray(card.rules) && card.rules.length > 0)
.map(card => card.rules.join('&'))
.join('>')
emit('change', groupInfo.value, props.group.name)
@@ -284,7 +282,7 @@ function onClose() {
<VBtn color="primary" variant="tonal" @click="addFilterCard">
<VIcon icon="mdi-plus" />
</VBtn>
<VBtn color="success" variant="tonal" @click="importRules">
<VBtn color="success" variant="tonal" @click="importRules('priority')">
<VIcon icon="mdi-import" />
</VBtn>
<VBtn color="info" variant="tonal" @click="shareRules">
@@ -295,8 +293,13 @@ function onClose() {
</VCardActions>
</VCard>
</VDialog>
<VDialog v-model="importCodeDialog" width="60rem" scrollable>
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
</VDialog>
<ImportCodeDialog
v-if="importCodeDialog"
v-model="importCodeDialog"
title="导入规则优先级"
:dataType="importCodeType"
@close="importCodeDialog = false"
@save="saveCodeString"
/>
</div>
</template>

View File

@@ -422,7 +422,6 @@ function onRemoveSubscribe() {
aspect-ratio="2/3"
:src="getImgUrl"
class="object-cover aspect-w-2 aspect-h-3"
:class="hover.isHovering ? 'on-hover' : ''"
cover
@load="isImageLoaded = true"
@error="imageLoadError = true"
@@ -433,6 +432,24 @@ function onRemoveSubscribe() {
</div>
</template>
</VImg>
<!-- 详情 -->
<VCardText
v-show="hover.isHovering || imageLoadError"
class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)"
>
<span class="font-bold">{{ props.media?.year }}</span>
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.title }}
</h1>
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
{{ props.media?.overview }}
</p>
<div class="flex align-center justify-between">
<IconBtn icon="mdi-magnify" color="white" @click.stop="handleSearch" />
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
</div>
</VCardText>
<!-- 类型角标 -->
<VChip
v-show="isImageLoaded"
@@ -455,23 +472,7 @@ function onRemoveSubscribe() {
>
{{ props.media?.vote_average }}
</VChip>
<!-- 详情 -->
<VCardText
v-show="hover.isHovering || imageLoadError"
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
>
<span class="font-bold">{{ props.media?.year }}</span>
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.title }}
</h1>
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
{{ props.media?.overview }}
</p>
<div class="flex align-center justify-between">
<IconBtn icon="mdi-magnify" color="white" @click.stop="handleSearch" />
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
</div>
</VCardText>
<!--来源图标-->
<VAvatar
size="24"
density="compact"
@@ -551,9 +552,3 @@ function onRemoveSubscribe() {
@remove="onRemoveSubscribe"
/>
</template>
<style lang="scss">
.on-hover img {
@apply brightness-50;
}
</style>

View File

@@ -144,10 +144,17 @@ onMounted(() => {
<AliyunAuthDialog
v-if="aliyunAuthDialog"
v-model="aliyunAuthDialog"
:conf="props.storage.config || {}"
@close="aliyunAuthDialog = false"
@done="handleDone"
/>
<U115AuthDialog v-if="u115AuthDialog" v-model="u115AuthDialog" @close="u115AuthDialog = false" @done="handleDone" />
<U115AuthDialog
v-if="u115AuthDialog"
v-model="u115AuthDialog"
:conf="props.storage.config || {}"
@close="u115AuthDialog = false"
@done="handleDone"
/>
<RcloneConfigDialog
v-if="rcloneConfigDialog"
v-model="rcloneConfigDialog"

View File

@@ -38,6 +38,9 @@ const subscribeFilesDialog = ref(false)
// 分享订阅弹窗
const subscribeShareDialog = ref(false)
// 定义一个变量来保存当前的订阅状态
const subscribeState = ref<string>(props.media?.state ?? 'P')
// 上一次更新时间
const lastUpdateText = ref(props.media && props.media.last_update ? formatDateDifference(props.media.last_update) : '')
@@ -81,6 +84,32 @@ async function searchSubscribe() {
}
}
// 切换订阅状态
async function toggleSubscribeStatus(state: 'R' | 'S') {
try {
// 根据传入的 state 判断对应的操作文字
const action = state === 'S' ? '暂停' : '启用'
// 弹出确认框
const isConfirmed = await createConfirm({
title: `确认${action}`,
content: `是否${action}订阅 ${props.media?.name}`,
})
if (!isConfirmed) return
// 调用 API 更新订阅状态
const result: { [key: string]: any } = await api.put(`subscribe/status/${props.media?.id}?state=${state}`)
// 提示
if (result.success) {
$toast.success(`${props.media?.name}${action}`)
subscribeState.value = state
emit('save')
} else {
$toast.error(`${action}失败:${result.message}`)
}
} catch (e) {
console.log(e)
}
}
// 重置订阅
async function resetSubscribe() {
// 确认
@@ -129,7 +158,7 @@ async function viewSubscribeFiles() {
}
// 弹出菜单
const dropdownItems = ref([
const dropdownItems = computed(() => [
{
title: '编辑',
value: 1,
@@ -163,8 +192,17 @@ const dropdownItems = ref([
},
},
{
title: '重置',
title: subscribeState.value === 'S' ? '启用' : '暂停',
value: 5,
props: {
prependIcon: subscribeState.value === 'S' ? 'mdi-play' : 'mdi-pause',
click: () => toggleSubscribeStatus(subscribeState.value === 'S' ? 'R' : 'S'),
color: subscribeState.value === 'S' ? 'success' : 'info',
},
},
{
title: '重置',
value: 6,
props: {
prependIcon: 'mdi-restore-alert',
click: resetSubscribe,
@@ -174,7 +212,7 @@ const dropdownItems = ref([
},
{
title: '分享',
value: 6,
value: 7,
props: {
prependIcon: 'mdi-share',
click: shareSubscribe,
@@ -184,7 +222,7 @@ const dropdownItems = ref([
},
{
title: '取消订阅',
value: 7,
value: 8,
props: {
prependIcon: 'mdi-trash-can-outline',
color: 'error',
@@ -242,6 +280,7 @@ function onSubscribeEditRemove() {
:class="{
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'opacity-70': subscribeState === 'S',
}"
min-height="170"
@click="editSubscribeDialog"
@@ -277,6 +316,10 @@ function onSubscribeEditRemove() {
</template>
<div class="absolute inset-0 subscribe-card-background"></div>
</VImg>
<div
v-if="subscribeState === 'P'"
class="absolute inset-0 bg-yellow-900 opacity-40 pointer-events-none"
></div>
</template>
<div>
<VCardText class="flex items-center">

View File

@@ -13,6 +13,9 @@ const props = defineProps({
torrent: Object as PropType<TorrentInfo>,
})
// 定义成功和失败事件
const emit = defineEmits(['done', 'error', 'close'])
// 提示框
const $toast = useToast()
@@ -22,8 +25,8 @@ const selectedDownloader = ref<string | null>(null)
// 选择的保存目录
const selectedDirectory = ref<string | null>(null)
// 定义成功和失败事件
const emit = defineEmits(['done', 'error', 'close'])
// 下载器
const downloaders = ref<DownloaderConf[]>([])
// 所有目录设置
const directories = ref<TransferDirectoryConf[]>([])
@@ -53,14 +56,10 @@ const targetDirectories = computed(() => {
return [...new Set(downloadDirectories)]
})
// 下载器
const downloaders = ref<DownloaderConf[]>([])
// 调用API查询下载器设置
async function loadDownloaderSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
downloaders.value = result.data?.value ?? []
downloaders.value = await api.get('download/clients')
} catch (error) {
console.log(error)
}

View File

@@ -2,6 +2,14 @@
import QrcodeVue from 'qrcode.vue'
import api from '@/api'
// 定义输入
const props = defineProps({
conf: {
type: Object as PropType<{ [key: string]: any }>,
required: true,
},
})
// 定义事件
const emit = defineEmits(['done', 'close'])
@@ -25,6 +33,10 @@ let timeoutTimer: NodeJS.Timeout | undefined = undefined
// 完成
async function handleDone() {
clearTimeout(timeoutTimer)
if (props.conf?.refreshToken) {
await savaAliPanConfig()
}
emit('done')
}
@@ -75,6 +87,15 @@ async function checkQrcode() {
}
}
// 保存cookie设置
async function savaAliPanConfig() {
try {
await api.post(`storage/save/alipan`, props.conf)
} catch (e) {
console.error(e)
}
}
onMounted(async () => {
await getQrcode()
timeoutTimer = setTimeout(checkQrcode, 3000)
@@ -97,6 +118,13 @@ onUnmounted(() => {
<template #prepend />
</VAlert>
</VCardText>
<VCardText>
<VRow>
<VCol class="mt-2">
<VTextField label="自定义refreshToken" v-model="props.conf.refreshToken" outlined dense />
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>

View File

@@ -2,23 +2,24 @@
// 输入参数
const props = defineProps({
title: String,
dataType: String,
})
// 定义事件
const emit = defineEmits(['update:modelValue', 'close'])
// 代码
const codeString = ref('')
// 定义事件
const emit = defineEmits(['close', 'save'])
// 导入
function handleImport() {
emit('update:modelValue', codeString.value)
emit('save', props.dataType, codeString)
emit('close')
}
</script>
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VDialog width="40rem" scrollable max-height="85vh" persistent>
<VCard :title="props.title" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">

View File

@@ -2,11 +2,11 @@
import { useToast } from 'vue-toast-notification'
import MediaIdSelector from '../misc/MediaIdSelector.vue'
import api from '@/api'
import { storageOptions } from '@/api/constants'
import { storageOptions, transferTypeOptions } from '@/api/constants'
import { numberValidator } from '@/@validators'
import { useDisplay } from 'vuetify'
import ProgressDialog from './ProgressDialog.vue'
import { FileItem, TransferDirectoryConf } from '@/api/types'
import { FileItem, TransferDirectoryConf, TransferForm } from '@/api/types'
// 显示器宽度
const display = useDisplay()
@@ -66,20 +66,12 @@ const dialogTitle = computed(() => {
})
// 表单
const transferForm = reactive({
fileitem: {},
const transferForm = reactive<TransferForm>({
fileitem: {} as FileItem,
logid: 0,
target_storage: props.target_storage ?? 'local',
target_path: props.target_path ?? null,
tmdbid: null,
doubanid: null,
season: null,
type_name: '',
transfer_type: '',
episode_format: '',
episode_detail: '',
episode_part: '',
episode_offset: null,
target_path: '',
min_filesize: 0,
scrape: false,
from_history: false,
@@ -87,7 +79,6 @@ const transferForm = reactive({
// 所有媒体库目录
const directories = ref<TransferDirectoryConf[]>([])
// 查询目录
async function loadDirectories() {
try {
@@ -104,16 +95,32 @@ const targetDirectories = computed(() => {
return [...new Set(libraryDirectories)]
})
// 监听目的路径变化,自动查询目录削配置
watch(transferForm, async () => {
if (transferForm.target_path) {
const directory = directories.value.find(item => item.library_path === transferForm.target_path)
if (directory) {
transferForm.scrape = directory.scraping ?? false
transferForm.transfer_type = directory.transfer_type ?? ''
// 监听目的路径变化,配置默认值
watch(
() => transferForm.target_path,
async newPath => {
if (newPath) {
const directory = directories.value.find(item => item.library_path === newPath)
if (directory) {
transferForm.target_storage = directory.library_storage ?? 'local'
transferForm.transfer_type = transferForm.transfer_type || directory.transfer_type
transferForm.scrape = directory.scraping ?? false
transferForm.library_category_folder = directory.library_category_folder ?? false
transferForm.library_type_folder = directory.library_type_folder ?? false
} else {
transferForm.transfer_type = transferForm.transfer_type || 'copy'
transferForm.scrape = false
transferForm.library_category_folder = false
transferForm.library_type_folder = false
}
} else {
// 路径为空时, 恢复到`自动`条件
transferForm.transfer_type = ''
transferForm.library_type_folder = undefined
transferForm.library_category_folder = undefined
}
}
})
)
// 使用SSE监听加载进度
function startLoadingProgress() {
@@ -179,7 +186,7 @@ async function handleTransfer(item: FileItem) {
// 整理日志
async function handleTransferLog(logid: number) {
transferForm.logid = logid
transferForm.fileitem = {}
transferForm.fileitem = {} as FileItem
try {
const result: { [key: string]: any } = await api.post('transfer/manual', transferForm)
if (!result.success) $toast.error(`历史记录 ${logid} 重新整理失败:${result.message}`)
@@ -215,18 +222,15 @@ onMounted(() => {
<VSelect
v-model="transferForm.transfer_type"
label="整理方式"
:items="[
{ title: '默认', value: '' },
{ title: '移动', value: 'move' },
{ title: '复制', value: 'copy' },
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
]"
:items="transferTypeOptions"
hint="文件操作整理方式"
persistent-hint
/>
persistent-hint>
<template v-slot:selection="{ item }">
{{ transferForm.transfer_type === '' ? '自动' : item.title }}
</template>
</VSelect>
</VCol>
<VCol cols="12" md="12">
<VCol cols="12">
<VCombobox
v-model="transferForm.target_path"
:items="targetDirectories"
@@ -337,6 +341,22 @@ onMounted(() => {
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6" v-if="transferForm.target_path">
<VSwitch
v-model="transferForm.library_type_folder"
label="按类型分类"
hint="整理时目的路径下按媒体类型添加子目录"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6" v-if="transferForm.target_path">
<VSwitch
v-model="transferForm.library_category_folder"
label="按类别分类"
hint="整理时在目的路径下按媒体类别添加子目录"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="transferForm.scrape"

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import type { Site } from '@/api/types'
import type { DownloaderConf, Site } from '@/api/types'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api'
@@ -35,6 +35,7 @@ const siteForm = ref<Site>({
limit_seconds: 0,
name: '',
domain: '',
downloader: '',
})
// 提示框
@@ -60,6 +61,24 @@ const priorityItems = ref(
})),
)
// 下载器选项
const downloaderOptions = ref<{ title: string; value: string }[]>([])
async function loadDownloaderSetting() {
try {
const downloaders: DownloaderConf[] = await api.get('download/clients')
downloaderOptions.value = [
{ title: '默认', value: '' },
...downloaders.map((item: { name: any }) => ({
title: item.name,
value: item.name,
})),
]
} catch (error) {
console.error('加载下载器设置失败:', error)
}
}
// 查询站点信息
async function fetchSiteInfo() {
try {
@@ -142,6 +161,7 @@ onMounted(async () => {
isLimit.value = true
if (siteForm.value.apikey) siteType.value = 'api'
}
await loadDownloaderSetting()
})
</script>
@@ -186,7 +206,7 @@ onMounted(async () => {
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="9">
<VCol cols="12" md="6">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
@@ -195,7 +215,21 @@ onMounted(async () => {
/>
</VCol>
<VCol cols="12" md="3">
<VTextField v-model="siteForm.timeout" label="超时时间(秒)" hint="站点请求超时时间" persistent-hint />
<VTextField
v-model="siteForm.timeout"
label="超时时间(秒)"
hint="站点请求超时时间为0时不限制"
persistent-hint
/>
</VCol>
<VCol cols="6" md="3">
<VSelect
v-model="siteForm.downloader"
label="下载器"
:items="downloaderOptions"
hint="此站点使用的下载器"
persistent-hint
/>
</VCol>
</VRow>
<VTabs v-model="siteType" show-arrows class="v-tabs-pill mt-3">

View File

@@ -2,7 +2,7 @@
import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators'
import api from '@/api'
import type { FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
import type { DownloaderConf, FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
import { useDisplay } from 'vuetify'
import { useConfirm } from 'vuetify-use-dialog'
import { VTextarea, VTextField } from 'vuetify/lib/components/index.mjs'
@@ -50,6 +50,7 @@ const subscribeForm = ref<Subscribe>({
sites: [],
best_version: undefined,
current_priority: 0,
downloader: '',
date: '',
show_edit_dialog: false,
})
@@ -57,6 +58,24 @@ const subscribeForm = ref<Subscribe>({
// 提示框
const $toast = useToast()
// 下载器选项
const downloaderOptions = ref<{ title: string; value: string }[]>([])
async function loadDownloaderSetting() {
try {
const downloaders: DownloaderConf[] = await api.get('download/clients')
downloaderOptions.value = [
{ title: '默认', value: '' },
...downloaders.map((item: { name: any }) => ({
title: item.name,
value: item.name,
})),
]
} catch (error) {
console.error('加载下载器设置失败:', error)
}
}
// 加载规则组
async function queryFilterRuleGroups() {
try {
@@ -188,7 +207,7 @@ async function removeSubscribe() {
// 查询下载目录
async function loadDownloadDirectories() {
try {
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
const result: { [key: string]: any } = await api.get('system/setting/Directories')
if (result.success && result.data?.value) {
downloadDirectories.value = result.data.value
}
@@ -200,8 +219,7 @@ async function loadDownloadDirectories() {
// 保存目录下拉框
const targetDirectories = computed(() => {
// 去重后的下载目录
const directories = downloadDirectories.value.map(item => item.download_path)
return [...new Set(directories)]
return downloadDirectories.value.map(item => item.download_path)
})
// 质量选择框数据
@@ -292,6 +310,7 @@ onMounted(() => {
queryFilterRuleGroups()
loadDownloadDirectories()
getSiteList()
loadDownloaderSetting()
if (props.subid) getSubscribeInfo()
if (props.default) queryDefaultSubscribeConfig()
})
@@ -392,6 +411,26 @@ onMounted(() => {
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="subscribeForm.downloader"
:items="downloaderOptions"
label="下载器"
hint="指定该订阅使用的下载器"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="subscribeForm.save_path"
:items="targetDirectories"
label="保存路径"
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="4">
<VSwitch
@@ -461,15 +500,6 @@ onMounted(() => {
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="subscribeForm.save_path"
:items="targetDirectories"
label="自定义保存路径"
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
persistent-hint
/>
</VCol>
</VRow>
<VRow v-if="!props.default">
<VCol cols="12">

View File

@@ -86,7 +86,7 @@ async function reSubscribe(item: Subscribe) {
else progressText.value = `正在重新订阅 ${item.name}${item.season} 季 ...`
progressDialog.value = true
try {
const result: { [key: string]: any } = await api.post('subscribe', item)
const result: { [key: string]: any } = await api.post('subscribe/', item)
if (result.success) {
emit('save')
}

View File

@@ -1,6 +1,15 @@
<script lang="ts" setup>
import QrcodeVue from 'qrcode.vue'
import api from '@/api'
import QrcodeVue from 'qrcode.vue'
import { VCardItem, VTextField } from 'vuetify/lib/components/index.mjs'
// 定义输入
const props = defineProps({
conf: {
type: Object as PropType<{ [key: string]: any }>,
required: true,
},
})
// 定义事件
const emit = defineEmits(['done', 'close'])
@@ -9,7 +18,7 @@ const emit = defineEmits(['done', 'close'])
const qrCodeContent = ref('')
// 下方的提示信息
const text = ref('请使用微信或115客户端扫码')
const text = ref('请使用微信或115客户端扫码或在下方输入Cookie')
// 提醒类型
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
@@ -19,6 +28,10 @@ let timeoutTimer: NodeJS.Timeout | undefined = undefined
// 完成
async function handleDone() {
clearTimeout(timeoutTimer)
if (props.conf?.cookie) {
await savaU115Config()
}
emit('done')
}
@@ -43,15 +56,21 @@ async function checkQrcode() {
if (result.success && result.data) {
const status = result.data.status
text.value = result.data.tip
if (status == 1) {
// 已确认完成
alertType.value = 'success'
handleDone()
} else if (status == 0) {
if (status == 0) {
alertType.value = 'info'
// 新建、待扫码
clearTimeout(timeoutTimer)
timeoutTimer = setTimeout(checkQrcode, 3000)
} else if (status == 1) {
// 已扫码
alertType.value = 'info'
text.value = '已扫码,请确认登录'
clearTimeout(timeoutTimer)
timeoutTimer = setTimeout(checkQrcode, 3000)
} else if (status == 2) {
// 已确认完成
alertType.value = 'success'
handleDone()
} else {
// 过期或者已取消
alertType.value = 'error'
@@ -65,6 +84,15 @@ async function checkQrcode() {
}
}
// 保存cookie设置
async function savaU115Config() {
try {
await api.post(`storage/save/u115`, props.conf)
} catch (e) {
console.error(e)
}
}
onMounted(async () => {
await getQrcode()
timeoutTimer = setTimeout(checkQrcode, 3000)
@@ -80,15 +108,20 @@ onUnmounted(() => {
<VCard title="115网盘登录" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 shadow-lg rounded border">
<VImg class="mx-auto" :src="qrCodeContent" style="block-size: 200px; inline-size: 200px">
<VSkeletonLoader v-if="!qrCodeContent" class="w-full h-full" />
</VImg>
<div class="my-6 shadow-lg rounded text-center p-3 border">
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" />
</div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />
</VAlert>
</VCardText>
<VCardText>
<VRow>
<VCol class="mt-2">
<VTextField label="自定义Cookie" v-model="props.conf.cookie" outlined dense />
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>

View File

@@ -0,0 +1,177 @@
<script lang="ts" setup>
import { isNullOrEmptyObject } from '@/@core/utils'
import api from '@/api'
import { useToast } from 'vue-toast-notification'
// 定义事件
const emit = defineEmits(['done', 'close'])
// 提示框
const $toast = useToast()
// 是否加载中
const loading = ref(false)
// 用户认证表单
const authForm = ref<any>({
site: null,
params: {},
})
// 所有认证站点
const authSites = ref<{
[key: string]: {
name: string
icon: string
params: { [key: string]: any }
}
}>({})
// 生成站点拉选项
const dropdownItems = computed(() => {
return Object.keys(authSites.value).map(key => {
return {
key,
name: authSites.value[key].name,
prependAvatar: authSites.value[key].icon,
}
})
})
// 读取authSites.params生成表单配置列表
const formFields = computed(() => {
const site = authSites.value[authForm.value.site]
return Object.keys(site?.params || {})
.filter(item => {
return site.params[item].name && site.params[item].type
})
.map(key => {
return {
key,
site: authForm.value.site,
name: site.params[key].name,
type: site.params[key].type,
placeholder: site.params[key].placeholder,
tooltip: site.params[key].tooltip,
}
})
})
// 查询之前使用的认证参数
async function loadLastAuthParams() {
try {
const result: { [key: string]: any } = await api.get(`system/setting/UserSiteAuthParams`)
if (result.success) {
const ret = result.data?.value
if (ret && !isNullOrEmptyObject(ret.params)) {
authForm.value = ret
}
}
} catch (e) {
console.error(e)
}
}
// 加载认证站点配置
async function loadAuthSites() {
try {
authSites.value = (await api.get(`site/auth`)) || {}
} catch (e) {
console.error(e)
}
}
// 完成
async function handleDone() {
await checkUser()
}
// 认证处理
async function checkUser() {
if (!authForm.value.site) {
$toast.error('请选择认证站点!')
return
}
if (!authSites.value[authForm.value.site]) {
$toast.error('站点配置不存在!')
return
}
if (formFields.value.length > 0) {
for (const field of formFields.value) {
if (!authForm.value.params[field.site.toUpperCase() + '_' + field.key.toUpperCase()]) {
$toast.error(`请输入${field.name}`)
return
}
}
}
loading.value = true
try {
const result: { [key: string]: any } = await api.post(`site/auth`, authForm.value)
if (result.success) {
$toast.success('用户认证成功,请重新登录!')
// 1秒后刷新页面
setTimeout(() => {
emit('done')
}, 1000)
} else {
$toast.error(`认证失败:${result.message}`)
}
} catch (e) {
console.error(e)
}
loading.value = false
}
onMounted(async () => {
await loadAuthSites()
loadLastAuthParams()
})
</script>
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard title="用户认证" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VCardText>
<VRow>
<VCol cols="12">
<VSelect
v-model="authForm.site"
:items="dropdownItems"
item-value="key"
item-title="name"
label="选择认证站点"
item-props
>
</VSelect>
</VCol>
</VRow>
<VRow>
<VCol v-for="param in formFields" :key="param.key">
<VTextField
v-model="authForm.params[param.site.toUpperCase() + '_' + param.key.toUpperCase()]"
:type="param.type"
:label="param.name"
:placeholder="param.placeholder"
:hint="param.tooltip"
clearable
persistent-hint
/>
</VCol>
</VRow>
</VCardText>
<VCardText class="text-center">
<VBtn
variant="elevated"
@click="handleDone"
prepend-icon="mdi-check"
class="px-5"
size="large"
:disabled="loading"
>
开始认证
</VBtn>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -1,9 +1,7 @@
<script setup lang="ts">
import api from '@/api'
import { FileItem } from '@/api/types'
import { VTreeview } from 'vuetify/labs/VTreeview'
// 输入变量为默认路径
const props = defineProps({
root: {
type: String,
@@ -16,16 +14,12 @@ const props = defineProps({
},
})
// update:modelValue 事件
const emit = defineEmits(['update:modelValue'])
// 激活的目录
const activedDirs = ref<string[]>([])
// 打开的目录
const openedDirs = ref<string[]>([])
const isUserAction = ref(false) // 标志:是否为用户主动操作
// 目录列表
const treeItems = ref<FileItem[]>([
{
name: '/',
@@ -37,19 +31,16 @@ const treeItems = ref<FileItem[]>([
},
])
// 拉取子目录
async function fetchDirs(item: any) {
return api
.post('/storage/list', item)
.then((data: any) => {
// 只添加目录到子目录
data = data.filter((i: any) => i.type === 'dir')
item.children.push(...data)
})
.catch(err => console.warn(err))
}
// 获取选择的目录路径
const selectedPath = computed(() => {
if (activedDirs.value.length > 0) {
return activedDirs.value[0]
@@ -57,13 +48,12 @@ const selectedPath = computed(() => {
return ''
})
// 监听目录变化
watch(activedDirs, newVal => {
if (!newVal.length) return
emit('update:modelValue', selectedPath)
if (!newVal.length || !isUserAction.value) return
emit('update:modelValue', selectedPath.value)
isUserAction.value = false
})
// 监听存储变化
watch(
() => props.storage,
async newVal => {
@@ -81,6 +71,10 @@ watch(
activedDirs.value = []
},
)
function handleUserSelect() {
isUserAction.value = true
}
</script>
<template>
@@ -102,7 +96,7 @@ watch(
max-height="20rem"
expand-icon="mdi-folder"
collapse-icon="mdi-folder-open"
>
</VTreeview>
@update:activated="handleUserSelect"
/>
</VMenu>
</template>

View File

@@ -6,6 +6,7 @@ import router from '@/router'
import avatar1 from '@images/avatars/avatar-1.png'
import api from '@/api'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue'
// Vuex Store
const store = useStore()
@@ -19,6 +20,9 @@ const $toast = useToast()
// 进度框
const progressDialog = ref(false)
// 站点认证对话框
const siteAuthDialog = ref(false)
// 执行注销操作
function logout() {
// 清除登录状态信息
@@ -56,10 +60,22 @@ async function restart() {
}
}
// 显示站点认证对话框
function showSiteAuthDialog() {
siteAuthDialog.value = true
}
// 用户站点认证成功
function siteAuthDone() {
siteAuthDialog.value = false
logout()
}
// 从Vuex Store中获取信息
const superUser = computed(() => store.state.auth.superUser)
const userName = computed(() => store.state.auth.userName)
const avatar = computed(() => store.state.auth.avatar || avatar1)
const userLevel = computed(() => store.state.auth.level)
</script>
<template>
@@ -94,6 +110,14 @@ const avatar = computed(() => store.state.auth.avatar || avatar1)
<VListItemTitle>个人信息</VListItemTitle>
</VListItem>
<!-- 👉 Site Auth -->
<VListItem v-if="userLevel < 2 && superUser" link @click="showSiteAuthDialog">
<template #prepend>
<VIcon class="me-2" icon="mdi-lock-check-outline" size="22" />
</template>
<VListItemTitle>用户认证</VListItemTitle>
</VListItem>
<!-- 👉 FAQ -->
<VListItem href="https://wiki.movie-pilot.org" target="_blank">
<template #prepend>
@@ -126,4 +150,6 @@ const avatar = computed(() => store.state.auth.avatar || avatar1)
</VAvatar>
<!-- 重启进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" text="正在重启 ..." />
<!-- 用户认证对话框 -->
<UserAuthDialog v-if="siteAuthDialog" v-model="siteAuthDialog" @done="siteAuthDone" @close="siteAuthDialog = false" />
</template>

View File

@@ -12,7 +12,6 @@ import { isPWA } from './@core/utils/navigator'
import './ace-config'
import { VAceEditor } from 'vue3-ace-editor'
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
import { VTreeview } from 'vuetify/labs/VTreeview'
import ToastPlugin from 'vue-toast-notification'
import VuetifyUseDialog from 'vuetify-use-dialog'
import VueApexCharts from 'vue3-apexcharts'
@@ -49,6 +48,11 @@ async function initializeApp() {
// 注册全局组件
initializeApp().then(() => {
// 优先注册框架
app
.use(vuetify)
// 注册全局组件
app
.component('VAceEditor', VAceEditor)
.component('VApexChart', VueApexCharts)
@@ -60,12 +64,10 @@ initializeApp().then(() => {
.component('VMediaInfoCard', MediaInfoCard)
.component('VTorrentCard', TorrentCard)
.component('VMediaIdSelector', MediaIdSelector)
.component('VTreeview', VTreeview)
.component('VPathField', PathField)
// 注册插件
app
.use(vuetify)
.use(router)
.use(store)
.use(ToastPlugin, {

View File

@@ -11,17 +11,12 @@ const activeTab = ref(route.query.tab)
// 下载器
const downloaders = ref<DownloaderConf[]>([])
// 获取启用的下载器
const enabledDownloaders = computed(() => downloaders.value.filter(item => item.enabled))
// 调用API查询下载器设置
async function loadDownloaderSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
if (result.data?.value && result.data.value.length > 0) {
downloaders.value = result.data?.value ?? []
if (!activeTab.value) activeTab.value = downloaders.value[0].name
}
downloaders.value = await api.get('download/clients')
if (downloaders.value && downloaders.value.length > 0 && !activeTab.value)
activeTab.value = downloaders.value[0].name
} catch (error) {
console.log(error)
}
@@ -31,21 +26,21 @@ function jumpTab(tab: string) {
router.push('/subscribe/movie?tab=' + tab)
}
onMounted(() => {
loadDownloaderSetting()
onMounted(async () => {
await loadDownloaderSetting()
})
</script>
<template>
<div v-if="enabledDownloaders.length > 0">
<div v-if="downloaders.length > 0">
<VTabs v-model="activeTab">
<VTab v-for="item in enabledDownloaders" :value="item.name" @to="jumpTab(item.name)">
<VTab v-for="item in downloaders" :value="item.name" @to="jumpTab(item.name)">
<span class="mx-5">{{ item.name }}</span>
</VTab>
</VTabs>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem v-for="item in enabledDownloaders" :value="item.name">
<VWindowItem v-for="item in downloaders" :value="item.name">
<transition name="fade-slide" appear>
<DownloadingListView :name="item.name" />
</transition>

View File

@@ -2,7 +2,7 @@
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
import SubscribeShareView from '@/views/subscribe/SubscribeShareView.vue'
import { SubscribeMovieTabs } from '@/router/menu'
import { SubscribeMovieTabs, SubscribeTvTabs } from '@/router/menu'
import router from '@/router'
const route = useRoute()
@@ -19,11 +19,11 @@ function jumpTab(tab: string) {
<template>
<div>
<VTabs v-model="activeTab">
<VTab v-for="item in SubscribeMovieTabs" :value="item.tab" @to="jumpTab(item.tab)">
<VTab v-if="subType == '电影'" v-for="item in SubscribeMovieTabs" :value="item.tab" @to="jumpTab(item.tab)">
<span class="mx-5">{{ item.title }}</span>
</VTab>
<VTab v-if="subType == '电视剧'" value="share" @to="jumpTab('share')">
<span class="mx-5">订阅分享</span>
<VTab v-if="subType == '电视剧'" v-for="item in SubscribeTvTabs" :value="item.tab" @to="jumpTab(item.tab)">
<span class="mx-5">{{ item.title }}</span>
</VTab>
</VTabs>

View File

@@ -219,6 +219,11 @@ export const SubscribeTvTabs = [
tab: 'popular',
icon: 'mdi-television',
},
{
title: '订阅分享',
tab: 'share',
icon: 'mdi-television',
},
]
// 插件标签页

View File

@@ -446,31 +446,32 @@ onBeforeMount(async () => {
</VWindow>
</div>
<!-- 插件搜索图标 -->
<VFab
icon="mdi-magnify"
color="info"
location="bottom"
size="x-large"
fixed
app
appear
@click="SearchDialog = true"
:class="appMode ? 'mb-28' : 'mb-16'"
/>
<!-- 插件市场设置图标 -->
<VFab
icon="mdi-store-cog"
color="warning"
location="bottom"
size="x-large"
fixed
app
appear
@click="MarketSettingDialog = true"
:class="{ 'mb-12': appMode }"
/>
<div>
<!-- 插件搜索图标 -->
<VFab
icon="mdi-magnify"
color="info"
location="bottom"
size="x-large"
fixed
app
appear
@click="SearchDialog = true"
:class="appMode ? 'mb-28' : 'mb-16'"
/>
<!-- 插件市场设置图标 -->
<VFab
icon="mdi-store-cog"
color="warning"
location="bottom"
size="x-large"
fixed
app
appear
@click="MarketSettingDialog = true"
:class="{ 'mb-12': appMode }"
/>
</div>
<!-- 插件市场设置窗口 -->
<PluginMarketSettingDialog
v-if="MarketSettingDialog"

View File

@@ -28,6 +28,7 @@ const currentHistory = ref<TransferHistory>()
// 重新整理IDS
const redoIds = ref<number[]>([])
const redoTargetStorage = ref<string>()
// 已选中的数据
const selected = ref<TransferHistory[]>([])
@@ -301,6 +302,7 @@ const dropdownItems = ref([
prependIcon: 'mdi-redo-variant',
click: (item: TransferHistory) => {
redoIds.value = [item.id]
redoTargetStorage.value = item.dest_storage
redoDialog.value = true
},
},
@@ -479,7 +481,7 @@ onMounted(fetchData)
</VCard>
<!-- 底部操作按钮 -->
<span>
<div>
<VFab
v-if="selected.length > 0"
icon="mdi-trash-can-outline"
@@ -503,7 +505,7 @@ onMounted(fetchData)
appear
@click="retransferBatch"
/>
</span>
</div>
<!-- 底部弹窗 -->
<VBottomSheet v-model="deleteConfirmDialog" inset>
<VCard class="text-center rounded-t">
@@ -530,6 +532,7 @@ onMounted(fetchData)
v-if="redoDialog"
v-model="redoDialog"
:logids="redoIds"
:target_storage="redoTargetStorage"
@done="transferDone"
@close="redoDialog = false"
/>

View File

@@ -3,18 +3,46 @@
import { useToast } from 'vue-toast-notification'
import { copyToClipboard } from '@/@core/utils/navigator'
import draggable from 'vuedraggable'
import { VRow } from 'vuetify/lib/components/index.mjs'
import api from '@/api'
import { CustomRule, FilterRuleGroup } from '@/api/types'
import CustomerRuleCard from '@/components/cards/CustomRuleCard.vue'
import FilterRuleGroupCard from '@/components/cards/FilterRuleGroupCard.vue'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
import internal from 'stream'
// 自定义规则列表
const customRules = ref<CustomRule[]>([])
// 自定义规则类型,仅用于导入判断
const customRulesType = ref<CustomRule>({
// 规则ID
id: '',
// 名称
name: '',
// 包含
include: '',
// 排除
exclude: '',
// 大小范围
size_range: '',
// 最少做种人数
seeders: '',
// 发布时间
publish_time: '',
})
// 所有规则组列表
const filterRuleGroups = ref<FilterRuleGroup[]>([])
// 规则组类型,仅用于导入判断
const filterRuleGroupsType = ref<FilterRuleGroup>({
// 名称
name: '',
// 规则串
rule_string: '',
// 适用类媒体类型 None-全部 电影/电视剧
media_type: '',
// # 适用媒体类别 None-全部 对应二级分类
category: '',
})
// 种子优先规则
const selectedTorrentPriority = ref<string>('seeder')
@@ -25,9 +53,6 @@ const mediaCategories = ref<{ [key: string]: any }>({})
// 导入代码弹窗
const importCodeDialog = ref(false)
// 导入的代码
const importCodeString = ref('')
// 导入代码类型
const importCodeType = ref('')
@@ -171,48 +196,96 @@ function shareRules(rules: CustomRule[] | FilterRuleGroup[]) {
}
}
// 导入规则
// 打开弹窗
async function importRules(ruleType: string) {
importCodeType.value = ruleType
importCodeString.value = ''
importCodeDialog.value = true
}
// 监听导入代码变化
watchEffect(() => {
if (!importCodeString.value) return
// 导入代码需要json格式
// 保存导入代码
function saveCodeString(type: string, codeString: any) {
// codeString从子组件传递过来从对象转换为JSON
let parsedCode
try {
if (importCodeType.value === 'custom') {
// 将导入的代码转换为规则卡片,并追加到已有的 customRules
const newCustomRules = JSON.parse(importCodeString.value).map((item: any) => {
return {
id: item.id,
name: item.name,
include: item.include,
exclude: item.exclude,
publish_time: item.publish_time,
seeders: item.seeders,
size_range: item.size_range,
}
})
customRules.value = [...customRules.value, ...newCustomRules] // 合并已有的和新导入的规则
} else if (importCodeType.value === 'group') {
// 将导入的代码转换为规则卡片,并追加到已有的 filterRuleGroups
const newFilterRuleGroups = JSON.parse(importCodeString.value).map((item: any) => {
return {
name: item.name,
rule_string: item.rule_string,
media_type: item.media_type,
category: item.category,
}
})
filterRuleGroups.value = [...filterRuleGroups.value, ...newFilterRuleGroups] // 合并已有的和新导入的规则
}
} catch (error) {
$toast.error('规则导入失败!')
parsedCode = JSON.parse(codeString.value)
} catch (e) {
$toast.error('导入规则失败!无法解析输入的数据!')
console.error(e)
return
}
})
// 更新数据
try {
if (type === 'custom') {
for (const value of parsedCode) {
if (!validateValueAgainstInterface(value, customRulesType)) return false
}
const newCustomRules = extractCustomRules(parsedCode) || []
customRules.value = [...customRules.value, ...newCustomRules]
} else if (type === 'group') {
for (const value of parsedCode) {
if (!validateValueAgainstInterface(value, filterRuleGroupsType)) return false
}
const newFilterRuleGroups = extractFilterRuleGroups(parsedCode) || []
filterRuleGroups.value = [...filterRuleGroups.value, ...newFilterRuleGroups]
} else {
$toast.error('导入规则失败!未知的数据类型!')
}
} catch (e) {
$toast.error('导入规则失败!')
console.error(e)
}
}
// 赋值自定义规则,避免存在多余的属性
function extractCustomRules(value: any) {
try {
return value.map((item: any) => {
return {
id: item.id,
name: item.name,
include: item.include,
exclude: item.exclude,
}
})
} catch (e) {
console.error(e)
}
}
// 赋值规则组,避免存在多余的属性
function extractFilterRuleGroups(value: any) {
try {
return value.map((item: any) => {
return {
name: item.name,
rule_string: item.rule_string,
media_type: item.media_type,
category: item.category,
}
})
} catch (e) {
console.error(e)
}
}
function validateValueAgainstInterface(value: any, interfaceDefinition: any): boolean {
console.log(value)
console.log(interfaceDefinition)
try {
// 循环判断是否命中全部接口定义,暂时只检查是否存在,不检查是否多出
for (const key in interfaceDefinition.value) {
if (value[key] === undefined) {
$toast.error(`导入规则失败!输入了不符合要求的数据!`)
return false
}
}
return true
} catch (e) {
console.error(e)
return false
}
}
// 规则变化时赋值
function onRuleChange(rule: CustomRule, id: string) {
@@ -368,12 +441,17 @@ onMounted(() => {
</div>
</VForm>
</VCardText>
<VDialog v-model="importCodeDialog" width="60rem" scrollable>
<ImportCodeDialog v-model="importCodeString" title="导入规则" @close="importCodeDialog = false" />
</VDialog>
</VCard>
</VCol>
</VRow>
<ImportCodeDialog
v-if="importCodeDialog"
v-model="importCodeDialog"
:title="`导入${importCodeType === 'custom' ? '自定义规则' : '规则组'}`"
:dataType="importCodeType"
@close="importCodeDialog = false"
@save="saveCodeString"
/>
<VRow>
<VCol cols="12">
<VCard>

View File

@@ -79,15 +79,7 @@ async function loadSiteSettings() {
// 将API返回的值赋值给SystemSettings
for (const sectionKey of Object.keys(siteSetting.value) as Array<keyof typeof siteSetting.value>) {
Object.keys(siteSetting.value[sectionKey]).forEach((key: string) => {
let v: any
if (result.data.hasOwnProperty(key)) {
v = result.data[key]
// 空字符串转为null避免空字符串导致前端显示问题
if (v === '') {
v = null
}
;(siteSetting.value[sectionKey] as any)[key] = v
}
if (result.data.hasOwnProperty(key)) (siteSetting.value[sectionKey] as any)[key] = result.data[key]
})
}
}
@@ -113,8 +105,10 @@ async function saveSiteSetting(value: { [key: string]: any }) {
try {
const result: { [key: string]: any } = await api.post('system/env', value)
if (result.success) {
$toast.success('保存设置成功')
$toast.success('保存站点设置成功')
await reloadSystem()
} else {
$toast.error('站点设置保存失败!')
}
} catch (error) {
console.log(error)

View File

@@ -28,6 +28,7 @@ const SystemSettings = ref<any>({
GLOBAL_IMAGE_CACHE: false,
BIG_MEMORY_MODE: false,
DB_WAL_ENABLE: false,
ENCODING_DETECTION_PERFORMANCE_MODE: true,
// 媒体
TMDB_API_DOMAIN: null,
TMDB_IMAGE_DOMAIN: null,
@@ -700,7 +701,7 @@ onDeactivated(() => {
<VWindowItem value="dev">
<div>
<VRow>
<VCol cols="12" md="6">
<VCol cols="12" md="4">
<VSwitch
v-model="SystemSettings.Advanced.DEBUG"
label="DEBUG日志"
@@ -708,7 +709,7 @@ onDeactivated(() => {
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VCol cols="12" md="4">
<VSwitch
v-model="SystemSettings.Advanced.PLUGIN_AUTO_RELOAD"
label="插件热加载"
@@ -716,6 +717,14 @@ onDeactivated(() => {
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSwitch
v-model="SystemSettings.Advanced.ENCODING_DETECTION_PERFORMANCE_MODE"
label="编码探测性能模式"
hint="优先提升探测效率,但可能降低编码探测的准确性"
persistent-hint
/>
</VCol>
</VRow>
</div>
</VWindowItem>

View File

@@ -100,29 +100,31 @@ onActivated(async () => {
/>
</VPullToRefresh>
<!-- 底部操作按钮 -->
<VFab
v-if="store.state.auth.superUser"
icon="mdi-clipboard-edit"
location="bottom"
size="x-large"
fixed
app
appear
@click="subscribeEditDialog = true"
:class="{ 'mb-12': appMode }"
/>
<VFab
v-if="store.state.auth.superUser"
icon="mdi-history"
color="info"
location="bottom"
:class="appMode ? 'mb-28' : 'mb-16'"
size="x-large"
fixed
app
appear
@click="historyDialog = true"
/>
<div>
<VFab
v-if="store.state.auth.superUser"
icon="mdi-clipboard-edit"
location="bottom"
size="x-large"
fixed
app
appear
@click="subscribeEditDialog = true"
:class="{ 'mb-12': appMode }"
/>
<VFab
v-if="store.state.auth.superUser"
icon="mdi-history"
color="info"
location="bottom"
:class="appMode ? 'mb-28' : 'mb-16'"
size="x-large"
fixed
app
appear
@click="historyDialog = true"
/>
</div>
<!-- 订阅编辑弹窗 -->
<SubscribeEditDialog
v-if="subscribeEditDialog"

View File

@@ -129,6 +129,26 @@ const targets = ref<Address[]>([
message: '未测试',
btndisable: false,
},
{
image: github,
name: 'api.github.com',
url: 'https://api.github.com',
proxy: true,
status: 'Normal',
time: '',
message: '未测试',
btndisable: false,
},
{
image: github,
name: 'raw.githubusercontent.com',
url: 'https://raw.githubusercontent.com',
proxy: true,
status: 'Normal',
time: '',
message: '未测试',
btndisable: false,
},
])
const resolveStatusColor: Status = {

View File

@@ -138,6 +138,15 @@ export default defineConfig({
},
},
build: {
minify: 'terser',
terserOptions: {
compress: {
// 控制console.log()是否被移除,生产环境建议移除,存在内存泄漏风险
drop_console: true,
// 控制debugger是否被移除酌情处理
drop_debugger: false,
},
},
chunkSizeWarningLimit: 5000,
cssCodeSplit: false,
rollupOptions: {

View File

@@ -7040,6 +7040,16 @@ terser@^5.17.4:
commander "^2.20.0"
source-map-support "~0.5.20"
terser@^5.36.0:
version "5.36.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e"
integrity sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==
dependencies:
"@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2"
commander "^2.20.0"
source-map-support "~0.5.20"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"