浏览器下载插件

This commit is contained in:
zhongluofeng
2026-07-21 17:40:55 +08:00
parent 548b022426
commit 141547acb6
9 changed files with 1322 additions and 129 deletions
+256 -31
View File
@@ -1,6 +1,7 @@
/**
* Thing Extension - Popup 脚本
* 配置 RPC 地址、密钥等参数,存储到 chrome.storage.local
* 1. RPC 配置(存储到 chrome.storage.local
* 2. 资源嗅探列表查看与下载
*/
const $ = (id) => document.getElementById(id)
@@ -11,9 +12,34 @@ const DEFAULT_CONFIG = {
interceptDownload: true,
minSize: 0,
excludeDomains: [],
showNotifications: true
showNotifications: true,
sniffEnabled: true,
sniffTypes: ['video', 'audio', 'image', 'archive', 'torrent', 'installer'],
sniffMaxItems: 200,
sniffMinSize: 1024 * 100,
sniffExcludeDomains: []
}
function send(type, payload) {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ type, ...payload }, resolve)
})
}
// ===== Tab 切换 =====
document.querySelectorAll('.tab').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(b => b.classList.remove('active'))
document.querySelectorAll('.panel').forEach(p => p.classList.add('hidden'))
btn.classList.add('active')
$('panel-' + btn.dataset.tab).classList.remove('hidden')
if (btn.dataset.tab === 'sniff') {
refreshSniffList()
}
})
})
// ===== 状态指示 =====
function setStatus(state, text) {
const dot = $('statusDot')
const txt = $('statusText')
@@ -21,20 +47,19 @@ function setStatus(state, text) {
txt.textContent = text
}
// ===== 配置表单 =====
async function loadConfig() {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ type: 'getConfig' }, (config) => {
resolve(config || { ...DEFAULT_CONFIG })
})
})
return send('getConfig', {})
}
function fillForm(config) {
$('rpcUrl').value = config.rpcUrl || DEFAULT_CONFIG.rpcUrl
$('rpcSecret').value = config.rpcSecret || ''
$('interceptDownload').checked = config.interceptDownload !== false
$('sniffEnabled').checked = config.sniffEnabled !== false
$('showNotifications').checked = config.showNotifications !== false
$('minSize').value = config.minSize || 0
$('sniffMinSize').value = config.sniffMinSize ?? DEFAULT_CONFIG.sniffMinSize
$('excludeDomains').value = (config.excludeDomains || []).join(',')
}
@@ -43,8 +68,10 @@ function readForm() {
rpcUrl: $('rpcUrl').value.trim() || DEFAULT_CONFIG.rpcUrl,
rpcSecret: $('rpcSecret').value.trim(),
interceptDownload: $('interceptDownload').checked,
sniffEnabled: $('sniffEnabled').checked,
showNotifications: $('showNotifications').checked,
minSize: parseInt($('minSize').value, 10) || 0,
sniffMinSize: parseInt($('sniffMinSize').value, 10) || 0,
excludeDomains: $('excludeDomains').value
.split(',')
.map(s => s.trim())
@@ -52,45 +79,243 @@ function readForm() {
}
}
document.getElementById('configForm').addEventListener('submit', async (e) => {
$('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 || '未知错误'))
}
})
const res = await send('saveConfig', { config })
if (res && res.ok) {
setStatus('', '已保存')
setTimeout(() => setStatus('', '配置已保存'), 1500)
} else {
setStatus('fail', '保存失败:' + (res?.error || '未知错误'))
}
})
$('testBtn').addEventListener('click', () => {
$('testBtn').addEventListener('click', async () => {
setStatus('', '测试中...')
// 先保存当前表单值,再测试
const config = readForm()
chrome.runtime.sendMessage({ type: 'saveConfig', config }, () => {
chrome.runtime.sendMessage({ type: 'testConnection' }, (res) => {
await send('saveConfig', { config })
const res = await send('testConnection', {})
if (res && res.ok) {
setStatus('ok', `已连接 · aria2 ${res.version}`)
} else {
setStatus('fail', '连接失败:' + (res?.error || '未知错误'))
}
})
// ===== 嗅探列表 =====
let allSniffed = {}
let currentSniffTab = 'video' // 当前选中的资源类型 tab
// "其他"包含:archive/torrent/installer
function getCategory(type) {
if (type === 'video' || type === 'audio' || type === 'image') return type
return 'other'
}
async function refreshSniffList() {
allSniffed = await send('getSniffed', {})
updateTabCounts()
renderSniffList()
}
function updateTabCounts() {
const counts = { video: 0, audio: 0, image: 0, other: 0 }
for (const tabId of Object.keys(allSniffed)) {
const tabData = allSniffed[tabId]
if (!tabData || !tabData.resources) continue
for (const r of tabData.resources) {
counts[getCategory(r.type)]++
}
}
$('count-video').textContent = counts.video
$('count-audio').textContent = counts.audio
$('count-image').textContent = counts.image
$('count-other').textContent = counts.other
}
function getFilteredResources() {
const all = []
for (const tabId of Object.keys(allSniffed)) {
const tabData = allSniffed[tabId]
if (!tabData || !tabData.resources) continue
for (const r of tabData.resources) {
if (getCategory(r.type) !== currentSniffTab) continue
all.push({ ...r, tabTitle: tabData.title || r.tabTitle || '' })
}
}
// 按时间倒序
all.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0))
return all
}
function renderSniffList() {
const list = $('sniffList')
const items = getFilteredResources()
if (items.length === 0) {
list.innerHTML = '<div class="empty">暂无嗅探记录</div>'
$('sniffStats').textContent = ''
return
}
list.innerHTML = items.map(r => {
// 图片:显示缩略图(可点击预览)
const isImage = r.type === 'image'
const isVideo = r.type === 'video'
const isPreviewable = isImage || isVideo
const thumbHtml = isImage
? `<img class="sniff-thumb" src="${escapeAttr(r.url)}" alt="" loading="lazy" data-preview="image" data-url="${escapeAttr(r.url)}" data-title="${escapeAttr(r.filename || '')}" onerror="this.style.display='none'">`
: ''
const previewBtn = isPreviewable
? `<button class="sniff-preview-btn" data-preview="${isVideo ? 'video' : 'image'}" data-url="${escapeAttr(r.url)}" data-title="${escapeAttr(r.filename || '')}">预览</button>`
: ''
return `
<div class="sniff-item">
<div class="sniff-item-head">
<span class="sniff-badge" style="background:${r.color || '#6b7280'}">${r.typeLabel || r.type}</span>
<span class="sniff-filename" title="${escapeHtml(r.filename || r.url)}">${escapeHtml(r.filename || '(未命名)')}</span>
<span class="sniff-size">${r.sizeText || '—'}</span>
</div>
<div class="sniff-url" title="${escapeHtml(r.url)}">${escapeHtml(r.url)}</div>
${thumbHtml}
<div class="sniff-actions">
<button class="dl" data-url="${escapeAttr(r.url)}" data-filename="${escapeAttr(r.filename || '')}" data-referer="${escapeAttr(r.tabUrl || '')}">下载</button>
${previewBtn}
<button class="copy" data-url="${escapeAttr(r.url)}">复制</button>
</div>
</div>
`
}).join('')
$('sniffStats').textContent = `${items.length}`
// 绑定下载按钮
list.querySelectorAll('.sniff-actions .dl').forEach(btn => {
btn.addEventListener('click', async () => {
const url = btn.dataset.url
const filename = btn.dataset.filename
const referer = btn.dataset.referer
btn.disabled = true
btn.textContent = '发送中...'
const res = await send('downloadSniffed', { url, filename, referer })
btn.disabled = false
btn.textContent = '下载'
if (res && res.ok) {
setStatus('ok', `已连接 · aria2 ${res.version}`)
btn.textContent = '已发送 ✓'
setTimeout(() => { btn.textContent = '下载' }, 1500)
} else {
setStatus('fail', '连接失败:' + (res?.error || '未知错误'))
alert('下载失败:' + (res?.error || '未知错误'))
}
})
})
// 绑定复制按钮
list.querySelectorAll('.sniff-actions .copy').forEach(btn => {
btn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(btn.dataset.url)
btn.textContent = '已复制 ✓'
setTimeout(() => { btn.textContent = '复制' }, 1200)
} catch {
alert('复制失败')
}
})
})
// 绑定预览按钮(图片缩略图和预览按钮共用)
list.querySelectorAll('[data-preview]').forEach(el => {
el.addEventListener('click', (e) => {
e.preventDefault()
const type = el.dataset.preview
const url = el.dataset.url
const title = el.dataset.title
openPreview(type, url, title)
})
})
}
// ===== 预览浮层 =====
function openPreview(type, url, title) {
const overlay = $('previewOverlay')
const body = $('previewBody')
const info = $('previewInfo')
if (type === 'image') {
body.innerHTML = `<img src="${escapeAttr(url)}" alt="${escapeAttr(title)}">`
} else if (type === 'video') {
body.innerHTML = `<video src="${escapeAttr(url)}" controls autoplay></video>`
}
info.textContent = `${title || ''} · ${url}`
overlay.classList.add('active')
}
function closePreview() {
const overlay = $('previewOverlay')
const body = $('previewBody')
// 释放视频资源
const video = body.querySelector('video')
if (video) video.pause()
body.innerHTML = ''
overlay.classList.remove('active')
}
$('previewClose').addEventListener('click', closePreview)
$('previewOverlay').addEventListener('click', (e) => {
// 点击遮罩关闭
if (e.target.id === 'previewOverlay') closePreview()
})
// ESC 关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && $('previewOverlay').classList.contains('active')) {
closePreview()
}
})
// 初始化
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function escapeAttr(s) {
return escapeHtml(s).replace(/'/g, '&#39;')
}
$('sniffRefreshBtn').addEventListener('click', async () => {
const btn = $('sniffRefreshBtn')
btn.disabled = true
btn.textContent = '嗅探中...'
await send('sniffCurrentTab', {})
await refreshSniffList()
btn.disabled = false
btn.textContent = '嗅探本页'
})
$('sniffClearBtn').addEventListener('click', async () => {
if (!confirm('清空所有嗅探记录?')) return
await send('clearSniffed', {})
await refreshSniffList()
})
// 子 Tab 切换
document.querySelectorAll('.sniff-tab').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.sniff-tab').forEach(b => b.classList.remove('active'))
btn.classList.add('active')
currentSniffTab = btn.dataset.type
renderSniffList()
})
})
// ===== 初始化 =====
;(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', '未连接')
}
})
const res = await send('testConnection', {})
if (res && res.ok) {
setStatus('ok', `已连接 · aria2 ${res.version}`)
} else {
setStatus('fail', '未连接')
}
// 加载嗅探列表
await refreshSniffList()
})()