下载模块 Init
This commit is contained in:
Generated
+165
-1
@@ -823,6 +823,15 @@ dependencies = [
|
||||
"dirs-sys 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
@@ -843,6 +852,18 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.4.6",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
@@ -2465,6 +2486,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
@@ -3088,6 +3110,30 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -3933,6 +3979,48 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"glob",
|
||||
"log",
|
||||
"objc2-foundation",
|
||||
"percent-encoding",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.18",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.5.4"
|
||||
@@ -4082,6 +4170,7 @@ name = "thing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
"futures-util",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -4090,6 +4179,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"tokio",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -5081,6 +5171,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -5129,13 +5228,30 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
@@ -5172,6 +5288,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -5190,6 +5312,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -5208,12 +5336,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -5232,6 +5372,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -5250,6 +5396,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -5268,6 +5420,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -5286,6 +5444,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
|
||||
@@ -20,14 +20,16 @@ tauri-build = { version = "2", features = [] }
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["tray-icon"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
chrono = "0.4"
|
||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||
futures-util = "0.3"
|
||||
tokio = { version = "1", features = ["io-util"] }
|
||||
tokio = { version = "1", features = ["io-util", "time"] }
|
||||
zip = "2"
|
||||
dirs = "5"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.52"
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"opener:allow-reveal-item-in-dir",
|
||||
"opener:allow-open-path",
|
||||
"dialog:default",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-close",
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Thing Extension - 后台 Service Worker
|
||||
*
|
||||
* 职责:
|
||||
* 1. 接管浏览器下载,转发到 aria2
|
||||
* 2. 与本应用(可选)通过 JSON-RPC 通信
|
||||
* 3. 提供右键菜单"使用 aria2 下载链接"
|
||||
* 4. 预留资源嗅探能力(webRequest 监听)
|
||||
*
|
||||
* 注:Service Worker 是短生命周期的,配置需持久化到 chrome.storage
|
||||
*/
|
||||
|
||||
// ===== 默认配置 =====
|
||||
const DEFAULT_CONFIG = {
|
||||
rpcUrl: 'http://127.0.0.1:6800/jsonrpc',
|
||||
rpcSecret: '',
|
||||
// 是否拦截浏览器原生下载
|
||||
interceptDownload: true,
|
||||
// 文件大小阈值(字节),超过才转 aria2。0 = 全部转
|
||||
minSize: 0,
|
||||
// 排除的域名(这些域名的下载走浏览器原生)
|
||||
excludeDomains: [],
|
||||
// 是否显示桌面通知
|
||||
showNotifications: true
|
||||
}
|
||||
|
||||
// ===== 配置读取 =====
|
||||
async function getConfig() {
|
||||
const stored = await chrome.storage.local.get('config')
|
||||
return { ...DEFAULT_CONFIG, ...(stored.config || {}) }
|
||||
}
|
||||
|
||||
async function saveConfig(config) {
|
||||
await chrome.storage.local.set({ config })
|
||||
}
|
||||
|
||||
// ===== aria2 JSON-RPC 调用 =====
|
||||
async function aria2Call(method, params = []) {
|
||||
const config = await getConfig()
|
||||
const rpcParams = []
|
||||
if (config.rpcSecret) {
|
||||
rpcParams.push(`token:${config.rpcSecret}`)
|
||||
}
|
||||
rpcParams.push(...params)
|
||||
|
||||
const resp = await fetch(config.rpcUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now().toString(),
|
||||
method,
|
||||
params: rpcParams
|
||||
})
|
||||
})
|
||||
if (!resp.ok) {
|
||||
throw new Error(`aria2 RPC HTTP ${resp.status}`)
|
||||
}
|
||||
const data = await resp.json()
|
||||
if (data.error) {
|
||||
throw new Error(`aria2 RPC error: ${data.error.message} (${data.error.code})`)
|
||||
}
|
||||
return data.result
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 URI 下载任务到 aria2
|
||||
* @returns {string} gid
|
||||
*/
|
||||
async function aria2AddUri(url, filename, referer, cookies) {
|
||||
const options = {}
|
||||
if (filename) options.out = filename
|
||||
if (referer) options.referer = referer
|
||||
if (cookies) options.header = [`Cookie: ${cookies}`]
|
||||
// User-Agent 用浏览器默认值更兼容
|
||||
options['user-agent'] = navigator.userAgent
|
||||
return aria2Call('aria2.addUri', [[url], options])
|
||||
}
|
||||
|
||||
// ===== 下载拦截 =====
|
||||
async function shouldIntercept(downloadItem) {
|
||||
const config = await getConfig()
|
||||
if (!config.interceptDownload) return false
|
||||
// 大小阈值
|
||||
if (config.minSize > 0 && downloadItem.fileSize > 0 && downloadItem.fileSize < config.minSize) {
|
||||
return false
|
||||
}
|
||||
// 域名排除
|
||||
try {
|
||||
const url = new URL(downloadItem.finalUrl || downloadItem.url)
|
||||
if (config.excludeDomains.some(d => url.hostname.includes(d))) {
|
||||
return false
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return true
|
||||
}
|
||||
|
||||
async function handleDownloadCreated(downloadItem) {
|
||||
if (!await shouldIntercept(downloadItem)) return
|
||||
|
||||
// 立即取消浏览器原生下载
|
||||
try {
|
||||
await chrome.downloads.cancel(downloadItem.id)
|
||||
await chrome.downloads.erase({ id: downloadItem.id })
|
||||
} catch { /* ignore */ }
|
||||
|
||||
const url = downloadItem.finalUrl || downloadItem.url
|
||||
const filename = downloadItem.filename || ''
|
||||
|
||||
try {
|
||||
const gid = await aria2AddUri(url, filename, downloadItem.referrer, '')
|
||||
await notify('已添加到 aria2', `${filename || url}\nGID: ${gid}`)
|
||||
} catch (e) {
|
||||
await notify('aria2 添加失败', `${filename || url}\n${e.message}`)
|
||||
// 失败时把 URL 重新交给浏览器下载
|
||||
try { await chrome.downloads.download({ url }) } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 通知 =====
|
||||
async function notify(title, message) {
|
||||
const config = await getConfig()
|
||||
if (!config.showNotifications) return
|
||||
try {
|
||||
await chrome.notifications.create({
|
||||
type: 'basic',
|
||||
iconUrl: 'icons/icon-128.png',
|
||||
title,
|
||||
message
|
||||
})
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// ===== 右键菜单 =====
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.contextMenus.create({
|
||||
id: 'thing-download-link',
|
||||
title: '使用 aria2 下载此链接',
|
||||
contexts: ['link']
|
||||
})
|
||||
chrome.contextMenus.create({
|
||||
id: 'thing-download-page',
|
||||
title: '使用 aria2 下载当前页面资源',
|
||||
contexts: ['page']
|
||||
})
|
||||
})
|
||||
|
||||
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
if (info.menuItemId === 'thing-download-link') {
|
||||
const url = info.linkUrl
|
||||
const filename = url.split('/').pop()?.split('?')[0] || ''
|
||||
try {
|
||||
const gid = await aria2AddUri(url, filename, info.pageUrl, '')
|
||||
await notify('已添加到 aria2', `${filename || url}\nGID: ${gid}`)
|
||||
} catch (e) {
|
||||
await notify('aria2 添加失败', `${e.message}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ===== 下载事件监听 =====
|
||||
chrome.downloads.onCreated.addListener(handleDownloadCreated)
|
||||
|
||||
// ===== 资源嗅探(预留,仅日志,不拦截)=====
|
||||
// 后续可启用:监听页面媒体资源,提供"嗅探到的资源"列表
|
||||
chrome.webRequest.onBeforeRequest.addListener(
|
||||
(details) => {
|
||||
// 预留:识别视频/音频流等可下载资源
|
||||
// 当前不处理,仅保留权限和入口
|
||||
return undefined
|
||||
},
|
||||
{ urls: ['<all_urls>'] },
|
||||
[]
|
||||
)
|
||||
|
||||
// ===== 来自 popup 的消息 =====
|
||||
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
if (msg.type === 'getConfig') {
|
||||
getConfig().then(sendResponse)
|
||||
return true
|
||||
}
|
||||
if (msg.type === 'saveConfig') {
|
||||
saveConfig(msg.config).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ ok: false, error: e.message }))
|
||||
return true
|
||||
}
|
||||
if (msg.type === 'testConnection') {
|
||||
aria2Call('aria2.getVersion', [])
|
||||
.then(res => sendResponse({ ok: true, version: res.version }))
|
||||
.catch(e => sendResponse({ ok: false, error: e.message }))
|
||||
return true
|
||||
}
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Thing Extension",
|
||||
"version": "0.1.0",
|
||||
"description": "接管浏览器下载,将任务发送到 aria2 内核。支持后续资源嗅探能力扩展。",
|
||||
"icons": {
|
||||
"16": "icons/icon-16.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"downloads",
|
||||
"storage",
|
||||
"notifications",
|
||||
"webRequest",
|
||||
"webNavigation",
|
||||
"contextMenus"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "icons/icon-16.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
},
|
||||
"default_title": "Thing Extension"
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "popup.html",
|
||||
"open_in_tab": false
|
||||
},
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self'; object-src 'self'"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 340px;
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
font-size: 13px;
|
||||
color: #1f2937;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.title h1 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
background: #f3f4f6;
|
||||
margin-bottom: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
.dot.ok {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.dot.fail {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.field span {
|
||||
font-size: 11px;
|
||||
color: #4b5563;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.field input {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.field input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
flex: 1;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #d1d5db;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.actions button:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.actions button.primary {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
border-color: #111827;
|
||||
}
|
||||
|
||||
.actions button.primary:hover {
|
||||
background: #1f2937;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Thing Extension</title>
|
||||
<link rel="stylesheet" href="popup.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<img src="icons/icon-48.png" alt="Thing" class="logo" />
|
||||
<div class="title">
|
||||
<h1>Thing Extension</h1>
|
||||
<span class="subtitle">aria2 下载接管</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="status" id="statusBox">
|
||||
<span class="dot" id="statusDot"></span>
|
||||
<span id="statusText">检测中...</span>
|
||||
</section>
|
||||
|
||||
<form id="configForm">
|
||||
<label class="field">
|
||||
<span>RPC 地址</span>
|
||||
<input type="text" id="rpcUrl" placeholder="http://127.0.0.1:6800/jsonrpc" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>RPC 密钥</span>
|
||||
<input type="password" id="rpcSecret" placeholder="未设置时留空" />
|
||||
</label>
|
||||
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="interceptDownload" />
|
||||
<span>接管浏览器下载</span>
|
||||
</label>
|
||||
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="showNotifications" />
|
||||
<span>显示桌面通知</span>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>最小文件大小(字节,0=全部)</span>
|
||||
<input type="number" id="minSize" min="0" placeholder="0" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>排除域名(逗号分隔)</span>
|
||||
<input type="text" id="excludeDomains" placeholder="例如:example.com,another.com" />
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" id="testBtn">测试连接</button>
|
||||
<button type="submit" class="primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Thing Extension - Popup 脚本
|
||||
* 配置 RPC 地址、密钥等参数,存储到 chrome.storage.local
|
||||
*/
|
||||
|
||||
const $ = (id) => document.getElementById(id)
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
rpcUrl: 'http://127.0.0.1:6800/jsonrpc',
|
||||
rpcSecret: '',
|
||||
interceptDownload: true,
|
||||
minSize: 0,
|
||||
excludeDomains: [],
|
||||
showNotifications: true
|
||||
}
|
||||
|
||||
function setStatus(state, text) {
|
||||
const dot = $('statusDot')
|
||||
const txt = $('statusText')
|
||||
dot.className = 'dot ' + (state === 'ok' ? 'ok' : state === 'fail' ? 'fail' : '')
|
||||
txt.textContent = text
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
return new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage({ type: 'getConfig' }, (config) => {
|
||||
resolve(config || { ...DEFAULT_CONFIG })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function fillForm(config) {
|
||||
$('rpcUrl').value = config.rpcUrl || DEFAULT_CONFIG.rpcUrl
|
||||
$('rpcSecret').value = config.rpcSecret || ''
|
||||
$('interceptDownload').checked = config.interceptDownload !== false
|
||||
$('showNotifications').checked = config.showNotifications !== false
|
||||
$('minSize').value = config.minSize || 0
|
||||
$('excludeDomains').value = (config.excludeDomains || []).join(',')
|
||||
}
|
||||
|
||||
function readForm() {
|
||||
return {
|
||||
rpcUrl: $('rpcUrl').value.trim() || DEFAULT_CONFIG.rpcUrl,
|
||||
rpcSecret: $('rpcSecret').value.trim(),
|
||||
interceptDownload: $('interceptDownload').checked,
|
||||
showNotifications: $('showNotifications').checked,
|
||||
minSize: parseInt($('minSize').value, 10) || 0,
|
||||
excludeDomains: $('excludeDomains').value
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('configForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault()
|
||||
const config = readForm()
|
||||
chrome.runtime.sendMessage({ type: 'saveConfig', config }, (res) => {
|
||||
if (res && res.ok) {
|
||||
setStatus('', '已保存')
|
||||
setTimeout(() => window.close(), 500)
|
||||
} else {
|
||||
setStatus('fail', '保存失败:' + (res?.error || '未知错误'))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
$('testBtn').addEventListener('click', () => {
|
||||
setStatus('', '测试中...')
|
||||
// 先保存当前表单值,再测试
|
||||
const config = readForm()
|
||||
chrome.runtime.sendMessage({ type: 'saveConfig', config }, () => {
|
||||
chrome.runtime.sendMessage({ type: 'testConnection' }, (res) => {
|
||||
if (res && res.ok) {
|
||||
setStatus('ok', `已连接 · aria2 ${res.version}`)
|
||||
} else {
|
||||
setStatus('fail', '连接失败:' + (res?.error || '未知错误'))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 初始化
|
||||
;(async () => {
|
||||
const config = await loadConfig()
|
||||
fillForm(config)
|
||||
// 自动测试一次连接
|
||||
setStatus('', '检测中...')
|
||||
chrome.runtime.sendMessage({ type: 'testConnection' }, (res) => {
|
||||
if (res && res.ok) {
|
||||
setStatus('ok', `已连接 · aria2 ${res.version}`)
|
||||
} else {
|
||||
setStatus('fail', '未连接')
|
||||
}
|
||||
})
|
||||
})()
|
||||
File diff suppressed because it is too large
Load Diff
+54
-3
@@ -1,9 +1,18 @@
|
||||
use tauri::Manager;
|
||||
|
||||
mod aria2_manager;
|
||||
mod logger;
|
||||
mod mihomo_manager;
|
||||
mod process_manager;
|
||||
|
||||
use aria2_manager::{
|
||||
Aria2Manager, downloader_add_uri, downloader_change_global_option, downloader_check_kernel_update,
|
||||
downloader_get_active, downloader_get_global_stat, downloader_get_rpc_info, downloader_get_settings,
|
||||
downloader_get_status, downloader_get_stopped, downloader_get_waiting, downloader_install_kernel,
|
||||
downloader_kernel_info, downloader_open_dir, downloader_open_url, downloader_pause, downloader_remove, downloader_restart, downloader_save_settings,
|
||||
downloader_start, downloader_status, downloader_stop, downloader_unpause, downloader_update_kernel,
|
||||
downloader_version,
|
||||
};
|
||||
use logger::{
|
||||
clear_logs, get_log_info, get_logs, log_message, LogManager,
|
||||
};
|
||||
@@ -28,9 +37,12 @@ fn greet(name: &str) -> String {
|
||||
fn quit_app(
|
||||
state: tauri::State<'_, ProcessManager>,
|
||||
mihomo: tauri::State<'_, MihomoManager>,
|
||||
aria2: tauri::State<'_, Aria2Manager>,
|
||||
) {
|
||||
// 退出前清理系统代理,避免遗留导致网络问题
|
||||
mihomo.cleanup_on_exit();
|
||||
// 退出前让 aria2 优雅关闭(保存 session)
|
||||
aria2.cleanup_on_exit();
|
||||
// 停止所有子进程
|
||||
state.stop_all();
|
||||
std::process::exit(0);
|
||||
@@ -41,6 +53,7 @@ pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_autostart::Builder::new().build())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.manage(ProcessManager::new())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
greet,
|
||||
@@ -77,7 +90,31 @@ pub fn run() {
|
||||
proxy_activate_profile,
|
||||
proxy_set_system_proxy,
|
||||
proxy_clear_system_proxy,
|
||||
proxy_get_system_proxy
|
||||
proxy_get_system_proxy,
|
||||
downloader_get_settings,
|
||||
downloader_save_settings,
|
||||
downloader_kernel_info,
|
||||
downloader_check_kernel_update,
|
||||
downloader_update_kernel,
|
||||
downloader_install_kernel,
|
||||
downloader_status,
|
||||
downloader_start,
|
||||
downloader_stop,
|
||||
downloader_restart,
|
||||
downloader_version,
|
||||
downloader_get_global_stat,
|
||||
downloader_get_active,
|
||||
downloader_get_waiting,
|
||||
downloader_get_stopped,
|
||||
downloader_get_status,
|
||||
downloader_add_uri,
|
||||
downloader_pause,
|
||||
downloader_unpause,
|
||||
downloader_remove,
|
||||
downloader_change_global_option,
|
||||
downloader_get_rpc_info,
|
||||
downloader_open_dir,
|
||||
downloader_open_url
|
||||
])
|
||||
.setup(|app| {
|
||||
// 初始化日志系统,日志目录: {app_data_dir}/logs/
|
||||
@@ -93,9 +130,13 @@ pub fn run() {
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||
let mihomo = MihomoManager::new(app_data_dir);
|
||||
let mihomo = MihomoManager::new(app_data_dir.clone());
|
||||
app.manage(mihomo);
|
||||
|
||||
// 初始化 Aria2Manager,数据目录: {app_data_dir}/downloader/
|
||||
let aria2 = Aria2Manager::new(app_data_dir);
|
||||
app.manage(aria2);
|
||||
|
||||
let open = tauri::menu::MenuItem::with_id(app, "open", "设置", true, None::<&str>)?;
|
||||
let quit = tauri::menu::MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
|
||||
let menu = tauri::menu::Menu::with_items(app, &[&open, &quit])?;
|
||||
@@ -112,10 +153,13 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
// 退出前清理系统代理 + 停止所有子进程
|
||||
// 退出前清理系统代理 + 优雅关闭 aria2 + 停止所有子进程
|
||||
if let Some(mihomo) = app.try_state::<MihomoManager>() {
|
||||
mihomo.cleanup_on_exit();
|
||||
}
|
||||
if let Some(aria2) = app.try_state::<Aria2Manager>() {
|
||||
aria2.cleanup_on_exit();
|
||||
}
|
||||
if let Some(pm) = app.try_state::<ProcessManager>() {
|
||||
pm.stop_all();
|
||||
}
|
||||
@@ -148,6 +192,13 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
// 应用启动时自动启动 aria2(如果用户在设置中开启了自动启动)
|
||||
if let Some(aria2) = app.try_state::<Aria2Manager>() {
|
||||
if let Some(pm) = app.try_state::<ProcessManager>() {
|
||||
aria2.auto_start_on_launch(app.handle(), &pm);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
|
||||
@@ -37,6 +37,6 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": ["binaries/*"]
|
||||
"resources": ["binaries/*", "resources/thing-extension/**/*"]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user