Files
Thing/src-tauri/resources/thing-extension/popup.js
T
2026-07-22 18:26:26 +08:00

322 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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, '&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('', '检测中...')
const res = await send('testConnection', {})
if (res && res.ok) {
setStatus('ok', '已连接 · Thing 下载引擎')
} else {
setStatus('fail', '未连接')
}
// 加载嗅探列表
await refreshSniffList()
})()