Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/aria2-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@115master/monkey": minor
---

新增 Aria2 RPC 导出功能:

- 文件列表每行在 115 原生下载按钮旁增加「推送 Aria2」按钮
- 支持配置多个 RPC 预设,按数量自适应(0=引导/1=单按钮/≥2=下拉)
- **支持文件夹递归下载**:自动遍历子目录,在 aria2 落盘时保留目录结构
- 独立设置面板:RPC 列表、下载路径、递归间隔、SHA1 校验、UA/Referer/自定义 Headers
- 新增通用 MasterToast 组件,支持 info / success / error / loading(带进度更新)
- 自动注入 115 的 Cookie / UA / Referer 到 aria2,绕过下载鉴权
1 change: 1 addition & 0 deletions apps/monkey/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"lodash": "^4.17.21",
"m3u8-parser": "^7.2.0",
"mitt": "^3.0.1",
"nanoid": "^5.1.9",
"photoswipe": "^5.4.4",
"tailwindcss": "^4.1.7",
"vue": "^3.5.13"
Expand Down
101 changes: 101 additions & 0 deletions apps/monkey/src/components/Aria2SettingsDialog/RpcListEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<template>
<div :class="$style.wrap">
<div
v-for="(item, i) in items"
:key="item.id"
:class="$style.row"
>
<input
:value="item.name"
class="input input-bordered input-sm w-40"
placeholder="名称"
@input="update(i, 'name', ($event.target as HTMLInputElement).value)"
>
<input
:value="item.url"
class="input input-bordered input-sm flex-1"
placeholder="http://token:密钥@127.0.0.1:6800/jsonrpc"
@input="update(i, 'url', ($event.target as HTMLInputElement).value)"
>
<button class="btn btn-sm btn-ghost" @click="onTest(i)">
测试
</button>
<button class="btn btn-sm btn-ghost text-error" @click="remove(i)">
删除
</button>
<span v-if="testResults[item.id]" :class="$style.result">
{{ testResults[item.id] }}
</span>
</div>
<button class="btn btn-sm btn-primary self-start" @click="add">
+ 添加 RPC
</button>
</div>
</template>

<script setup lang="ts">
import type { Aria2RpcPreset } from '@/utils/aria2'
import { nanoid } from 'nanoid'
import { reactive, watch } from 'vue'
import { testRpc } from '@/utils/aria2'

const props = defineProps<{ modelValue: Aria2RpcPreset[] }>()
const emit = defineEmits<{ (e: 'update:modelValue', v: Aria2RpcPreset[]): void }>()

const items = reactive<Aria2RpcPreset[]>([...props.modelValue])
const testResults = reactive<Record<string, string>>({})

watch(() => props.modelValue, (next) => {
items.splice(0, items.length, ...next)
}, { deep: true })

function emitChange() {
emit('update:modelValue', JSON.parse(JSON.stringify(items)))
}

function update(i: number, k: 'name' | 'url', v: string) {
items[i][k] = v
emitChange()
}

function add() {
items.push({ id: nanoid(), name: '', url: '' })
emitChange()
}

function remove(i: number) {
items.splice(i, 1)
emitChange()
}

async function onTest(i: number) {
const item = items[i]
testResults[item.id] = '连接中...'
try {
const v = await testRpc(item.url)
testResults[item.id] = `v${v}`
}
catch (e) {
testResults[item.id] = e instanceof Error ? e.message : '失败'
}
}
</script>

<style module>
.wrap {
display: flex;
flex-direction: column;
gap: 8px;
}
.row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.result {
font-size: 12px;
color: #64748b;
margin-left: 4px;
}
</style>
140 changes: 140 additions & 0 deletions apps/monkey/src/components/Aria2SettingsDialog/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<template>
<dialog ref="dialogRef" class="modal">
<div class="modal-box w-11/12 max-w-3xl">
<h3 class="font-bold text-lg mb-4">
Aria2 导出设置
</h3>

<div :class="$style.section">
<label class="label">RPC 列表</label>
<RpcListEditor v-model="form.rpcList" />
</div>

<div :class="$style.section">
<label class="label">下载路径(可选)</label>
<input
v-model="form.downloadPath"
class="input input-bordered w-full"
placeholder="绝对路径,对应 aria2 --dir"
>
</div>

<div :class="$style.section">
<label class="label">递归间隔(ms)</label>
<input
v-model.number="form.intervalMs"
type="number"
class="input input-bordered w-40"
min="0"
>
</div>

<div :class="$style.section">
<label class="label cursor-pointer justify-start gap-2">
<input
v-model="form.sha1Check"
type="checkbox"
class="checkbox checkbox-sm"
>
SHA1 校验
</label>
</div>

<div :class="$style.section">
<label class="label cursor-pointer justify-start gap-2">
<input
v-model="form.useBrowserUA"
type="checkbox"
class="checkbox checkbox-sm"
>
使用浏览器 UA
</label>
<input
v-model="form.userAgent"
class="input input-bordered w-full mt-2"
:disabled="form.useBrowserUA"
placeholder="自定义 User-Agent"
>
</div>

<div :class="$style.section">
<label class="label">Referer</label>
<input v-model="form.referer" class="input input-bordered w-full">
</div>

<div :class="$style.section">
<label class="label">自定义 Headers(每行 Key: Value)</label>
<textarea
v-model="form.extraHeaders"
class="textarea textarea-bordered w-full h-24"
/>
</div>

<div class="modal-action">
<button class="btn" @click="onReset">
重置
</button>
<button class="btn btn-primary" @click="onApply">
应用
</button>
<button class="btn" @click="close">
关闭
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</template>

<script setup lang="ts">
import type { Aria2Settings } from '@/utils/aria2'
import { onMounted, onUnmounted, reactive, ref } from 'vue'
import { ARIA2_DEFAULT_SETTINGS, aria2Events, readSettings, writeSettings } from '@/utils/aria2'
import RpcListEditor from './RpcListEditor.vue'

const dialogRef = ref<HTMLDialogElement | null>(null)
const form = reactive<Aria2Settings>({ ...readSettings() })

function refill() {
Object.assign(form, readSettings())
if (!form.userAgent)
form.userAgent = navigator.userAgent
}

function open() {
refill()
dialogRef.value?.showModal()
}

function close() {
dialogRef.value?.close()
}

function onApply() {
const cleaned: Aria2Settings = {
...form,
rpcList: form.rpcList.filter(r => r.name.trim() && r.url.trim()),
}
writeSettings(cleaned)
close()
}

function onReset() {
Object.assign(form, ARIA2_DEFAULT_SETTINGS)
}

onMounted(() => {
aria2Events.on('aria2:open-settings', open)
})
onUnmounted(() => {
aria2Events.off('aria2:open-settings', open)
})
</script>

<style module>
.section {
margin-bottom: 16px;
}
</style>
66 changes: 66 additions & 0 deletions apps/monkey/src/components/MasterToast/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<template>
<div :class="$style.container">
<TransitionGroup name="toast">
<div
v-for="t in toastQueue"
:key="t.id"
role="alert"
:class="[$style.toast, $style[t.kind]]"
>
<span
v-if="t.kind === 'loading'"
class="loading loading-spinner loading-sm" :class="[$style.icon]"
/>
<span>{{ t.message }}</span>
</div>
</TransitionGroup>
</div>
</template>

<script setup lang="ts">
import { toastQueue } from './toast'
</script>

<style module>
.container {
position: fixed;
right: 24px;
bottom: 24px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 99999;
pointer-events: none;
}

.toast {
pointer-events: auto;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: 10px;
color: white;
font-size: 14px;
min-width: 220px;
max-width: 400px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
}

.info {
background: #3b82f6;
}
.success {
background: #16a34a;
}
.error {
background: #dc2626;
}
.loading {
background: #475569;
}

.icon {
flex-shrink: 0;
}
</style>
Loading