322 lines
10 KiB
JavaScript
322 lines
10 KiB
JavaScript
/**
|
||
* Thing Extension - Popup 脚本
|
||
* 1. RPC 配置(存储到 chrome.storage.local)
|
||
* 2. 资源嗅探列表查看与下载
|
||
*/
|
||
|
||
const $ = (id) => document.getElementById(id)
|
||
|
||
const DEFAULT_CONFIG = {
|
||
serverUrl: 'http://127.0.0.1:16800',
|
||
secret: '',
|
||
interceptDownload: true,
|
||
minSize: 0,
|
||
excludeDomains: [],
|
||
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')
|
||
dot.className = 'dot ' + (state === 'ok' ? 'ok' : state === 'fail' ? 'fail' : '')
|
||
txt.textContent = text
|
||
}
|
||
|
||
// ===== 配置表单 =====
|
||
async function loadConfig() {
|
||
return send('getConfig', {})
|
||
}
|
||
|
||
function fillForm(config) {
|
||
$('serverUrl').value = config.serverUrl || DEFAULT_CONFIG.serverUrl
|
||
$('secret').value = config.secret || ''
|
||
$('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(',')
|
||
}
|
||
|
||
function readForm() {
|
||
return {
|
||
serverUrl: $('serverUrl').value.trim() || DEFAULT_CONFIG.serverUrl,
|
||
secret: $('secret').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())
|
||
.filter(Boolean)
|
||
}
|
||
}
|
||
|
||
$('configForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault()
|
||
const config = readForm()
|
||
const res = await send('saveConfig', { config })
|
||
if (res && res.ok) {
|
||
setStatus('', '已保存')
|
||
setTimeout(() => setStatus('', '配置已保存'), 1500)
|
||
} else {
|
||
setStatus('fail', '保存失败:' + (res?.error || '未知错误'))
|
||
}
|
||
})
|
||
|
||
$('testBtn').addEventListener('click', async () => {
|
||
setStatus('', '测试中...')
|
||
const config = readForm()
|
||
await send('saveConfig', { config })
|
||
const res = await send('testConnection', {})
|
||
if (res && res.ok) {
|
||
setStatus('ok', '已连接 · Thing 下载引擎')
|
||
} 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) {
|
||
btn.textContent = '已发送 ✓'
|
||
setTimeout(() => { btn.textContent = '下载' }, 1500)
|
||
} else {
|
||
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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
}
|
||
|
||
function escapeAttr(s) {
|
||
return escapeHtml(s).replace(/'/g, ''')
|
||
}
|
||
|
||
$('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('', '检测中...')
|
||
const res = await send('testConnection', {})
|
||
if (res && res.ok) {
|
||
setStatus('ok', '已连接 · Thing 下载引擎')
|
||
} else {
|
||
setStatus('fail', '未连接')
|
||
}
|
||
// 加载嗅探列表
|
||
await refreshSniffList()
|
||
})()
|