/** * 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 = '
暂无嗅探记录
' $('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 ? `` : '' const previewBtn = isPreviewable ? `` : '' return `
${r.typeLabel || r.type} ${escapeHtml(r.filename || '(未命名)')} ${r.sizeText || '—'}
${escapeHtml(r.url)}
${thumbHtml}
${previewBtn}
` }).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 = `${escapeAttr(title)}` } else if (type === 'video') { body.innerHTML = `` } 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, '"') } 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() })()