// ==UserScript== // @name 豆包视频&图片下载助手 // @namespace https://www.doubao.com/ // @version 5.0.0 // @description 自动提取豆包(Doubao)网站中的视频与图片链接,支持视频/图片预览、一键复制、批量下载,带可拖拽悬浮按钮和红点提醒 // @author Doubao Downloader // @match https://www.doubao.com/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_notification // @run-at document-end // @noframes // @supportURL https://github.com // @homepageURL https://github.com // @license MIT // // 免责声明:本脚本仅供学习和参考,切勿用于违法或商用,否则后果自负。 // // ==/UserScript== (function () { 'use strict'; const videoLinks = new Map(); const imageLinks = new Map(); const processedVideoSrcs = new Set(); const processedImageSrcs = new Set(); let activeTab = 'video'; GM_addStyle(` #db-dl-btn, #db-dl-panel { --bg: #ffffff; --bg-sub: #f8fafc; --bg-elevated: #f1f5f9; --bg-hover: #f1f5f9; --border: #e2e8f0; --border-strong: #cbd5e1; --text: #0f172a; --text-sub: #475569; --text-muted: #94a3b8; --primary: #2563eb; --primary-hover: #1d4ed8; --primary-soft: #eff6ff; --danger: #ef4444; --danger-soft: #fef2f2; --danger-hover: #dc2626; --success: #10b981; --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04); --shadow: 0 2px 8px rgba(15, 23, 42, 0.06); --shadow-lg: -8px 0 32px rgba(15, 23, 42, 0.12); } #db-dl-panel, #db-dl-btn, #db-dl-toast { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } #db-dl-btn { position: fixed; bottom: 24px; right: 24px; z-index: 2147483646; width: 48px; height: 48px; border-radius: 12px; background: #ffffff; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18); cursor: move; display: flex; align-items: center; justify-content: center; transition: transform 0.2s ease, box-shadow 0.2s ease; border: 1px solid var(--border); outline: none; user-select: none; -webkit-tap-highlight-color: transparent; } #db-dl-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(15, 23, 42, 0.25); } #db-dl-btn:active { transform: translateY(0); cursor: grabbing; } #db-dl-btn img { width: 32px; height: 32px; pointer-events: none; border-radius: 6px; } #db-dl-btn .badge { position: absolute; top: -4px; right: -4px; width: 14px; height: 14px; border-radius: 50%; background: #ef4444; box-shadow: 0 0 0 2px #ffffff, 0 2px 8px rgba(239, 68, 68, 0.5); pointer-events: none; transition: transform 0.2s ease; z-index: 10; } #db-dl-btn .badge.hidden { display: none; } #db-dl-btn .badge.pulse { animation: db-badge-pulse 1.6s ease-in-out infinite; } @keyframes db-badge-pulse { 0%,100% { box-shadow: 0 0 0 2px #ffffff, 0 2px 6px rgba(239, 68, 68, 0.4); transform: scale(1); } 50% { box-shadow: 0 0 0 2px #ffffff, 0 0 0 8px rgba(239, 68, 68, 0.15), 0 2px 8px rgba(239, 68, 68, 0.5); transform: scale(1.05); } } #db-dl-overlay { position: fixed; inset: 0; z-index: 2147483647; background: rgba(15, 23, 42, 0.4); opacity: 0; pointer-events: none; transition: opacity 0.25s ease; } #db-dl-overlay.show { opacity: 1; pointer-events: auto; } #db-dl-panel { position: fixed; top: 0; right: 0; bottom: 0; width: 480px; max-width: 92vw; z-index: 2147483648; background: var(--bg); color: var(--text); box-shadow: var(--shadow-lg); transform: translateX(105%); transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1); display: flex; flex-direction: column; font-size: 14px; } #db-dl-panel.open { transform: translateX(0); } .db-panel-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 16px 0 20px; flex-shrink: 0; background: var(--bg); } .db-panel-header .title { font-size: 16px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 8px; letter-spacing: -0.01em; flex: 1; min-width: 0; } .db-panel-header .title svg { width: 22px; height: 22px; flex-shrink: 0; border-radius: 5px; } .db-panel-header .title-text { flex-shrink: 0; } .db-panel-header .count-tag { background: var(--primary); color: #ffffff; font-size: 11px; font-weight: 700; padding: 3px 9px; border-radius: 10px; min-width: 24px; text-align: center; flex-shrink: 0; } .db-panel-header .close-btn { width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg); color: var(--text-sub); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; line-height: 1; font-size: 18px; flex-shrink: 0; } .db-panel-header .close-btn:hover { background: var(--danger-soft); color: var(--danger); border-color: var(--danger); } .db-panel-tabs { display: flex; gap: 4px; padding: 12px 20px 0; flex-shrink: 0; background: var(--bg); } .db-panel-tabs button { flex: 1; padding: 8px 12px; border: none; background: transparent; color: var(--text-sub); font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; border-radius: 8px 8px 0 0; font-family: inherit; display: flex; align-items: center; justify-content: center; gap: 6px; border-bottom: 2px solid transparent; } .db-panel-tabs button svg { width: 14px; height: 14px; fill: currentColor; } .db-panel-tabs button:hover { color: var(--text); background: var(--bg-hover); } .db-panel-tabs button.active { color: var(--primary); border-bottom-color: var(--primary); } .db-panel-tabs .tab-count { background: var(--bg-elevated); color: var(--text-sub); font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 8px; min-width: 18px; text-align: center; } .db-panel-tabs button.active .tab-count { background: var(--primary-soft); color: var(--primary); } .db-panel-toolbar { display: flex; gap: 6px; padding: 10px 16px; flex-shrink: 0; background: var(--bg); border-bottom: 1px solid var(--border); } .db-panel-toolbar button { flex: 1; padding: 7px 0; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--text-sub); font-size: 12px; cursor: pointer; transition: all 0.15s ease; font-weight: 500; font-family: inherit; display: flex; align-items: center; justify-content: center; gap: 4px; } .db-panel-toolbar button svg { width: 12px; height: 12px; fill: currentColor; } .db-panel-toolbar button:hover { background: var(--primary-soft); color: var(--primary); border-color: var(--primary); } .db-panel-toolbar button.primary { background: var(--primary); color: #fff; border-color: var(--primary); box-shadow: 0 1px 2px rgba(37, 99, 235, 0.25); } .db-panel-toolbar button.primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); color: #fff; } .db-panel-list { flex: 1; overflow-y: auto; padding: 12px; overscroll-behavior: contain; background: var(--bg-sub); } .db-panel-list::-webkit-scrollbar { width: 6px; } .db-panel-list::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; } .db-panel-list::-webkit-scrollbar-track { background: transparent; } .db-disclaimer { flex-shrink: 0; padding: 10px 16px; border-top: 1px solid var(--border); font-size: 11px; color: var(--text-muted); text-align: center; line-height: 1.5; background: var(--bg); } .db-empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-muted); text-align: center; padding: 40px 20px; gap: 6px; } .db-empty-state .icon-wrap { width: 64px; height: 64px; border-radius: 16px; background: var(--bg); border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; margin-bottom: 14px; } .db-empty-state .icon-wrap svg { width: 28px; height: 28px; fill: var(--primary); opacity: 0.5; } .db-empty-state .hint { font-size: 12px; color: var(--text-muted); margin-top: 2px; } .db-video-card, .db-image-card { background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 12px; margin-bottom: 10px; transition: border-color 0.15s ease, box-shadow 0.15s ease; animation: db-card-in 0.3s ease; position: relative; } @keyframes db-card-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } @keyframes db-card-out { to { opacity: 0; transform: translateX(40px); height: 0; padding: 0; margin: 0; border-width: 0; } } .db-video-card:hover, .db-image-card:hover { border-color: var(--primary); box-shadow: 0 2px 12px rgba(37, 99, 235, 0.08); } .db-card-row { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; } .db-thumb { width: 80px; height: 60px; border-radius: 6px; background: var(--bg-elevated); flex-shrink: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; border: 1px solid var(--border); } .db-thumb.video { height: 45px; } .db-thumb img { width: 100%; height: 100%; object-fit: cover; pointer-events: none; } .db-thumb svg { width: 22px; height: 22px; fill: var(--primary); opacity: 0.5; } .db-card-info { flex: 1; min-width: 0; } .db-card-info .meta { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-muted); margin-bottom: 4px; } .db-card-info .meta .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--border-strong); } .db-card-info .meta .badge-mini { background: var(--primary-soft); color: var(--primary); font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; } .db-card-info .filename { font-size: 12px; color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace; line-height: 1.4; } .db-delete-btn { position: absolute; top: 8px; right: 8px; width: 22px; height: 22px; border-radius: 5px; border: none; background: transparent; color: var(--text-muted); cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; transition: all 0.15s ease; } .db-video-card:hover .db-delete-btn, .db-image-card:hover .db-delete-btn { opacity: 1; } .db-delete-btn:hover { background: var(--danger-soft); color: var(--danger); } .db-delete-btn svg { width: 12px; height: 12px; fill: currentColor; } .db-card-actions { display: flex; gap: 6px; } .db-card-actions button { flex: 1; padding: 7px 0; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--text-sub); font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; display: flex; align-items: center; justify-content: center; gap: 4px; font-family: inherit; } .db-card-actions button svg { width: 13px; height: 13px; fill: currentColor; } .db-card-actions button.copy-btn:hover { background: var(--primary-soft); color: var(--primary); border-color: var(--primary); } .db-card-actions button.dl-btn { background: var(--primary); color: #fff; border-color: var(--primary); } .db-card-actions button.dl-btn:hover { background: var(--primary-hover); border-color: var(--primary-hover); } .db-card-actions button.copied { background: var(--success) !important; border-color: var(--success) !important; color: #fff !important; } #db-dl-toast { position: fixed; bottom: 88px; left: 50%; transform: translateX(-50%) translateY(8px); z-index: 2147483649; background: var(--text); color: var(--bg); padding: 9px 18px; border-radius: 8px; font-size: 12px; font-weight: 500; opacity: 0; pointer-events: none; transition: all 0.2s ease; white-space: nowrap; box-shadow: 0 4px 16px rgba(15, 23, 42, 0.2); } #db-dl-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } #db-preview-modal { position: fixed; inset: 0; z-index: 2147483650; background: rgba(0, 0, 0, 0.85); display: none; align-items: center; justify-content: center; padding: 32px; backdrop-filter: blur(8px); } #db-preview-modal.show { display: flex; animation: db-modal-in 0.25s ease; } @keyframes db-modal-in { from { opacity: 0; } to { opacity: 1; } } #db-preview-modal .preview-content { position: relative; max-width: 92vw; max-height: 92vh; display: flex; align-items: center; justify-content: center; } #db-preview-modal img, #db-preview-modal video { max-width: 92vw; max-height: 88vh; border-radius: 8px; box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5); background: #000; display: block; } #db-preview-modal .preview-close { position: fixed; top: 20px; right: 24px; width: 40px; height: 40px; border-radius: 50%; border: none; background: rgba(255, 255, 255, 0.15); color: #fff; font-size: 22px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.15s; z-index: 10; line-height: 1; } #db-preview-modal .preview-close:hover { background: rgba(255, 255, 255, 0.3); } #db-preview-modal .preview-info { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.7); color: #fff; padding: 10px 18px; border-radius: 8px; font-size: 12px; max-width: 80vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace; } #db-preview-modal video { outline: none; } .db-thumb { cursor: pointer; position: relative; } .db-thumb::after { content: '预览'; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; color: rgba(255, 255, 255, 0); letter-spacing: 0.5px; transition: all 0.15s ease; pointer-events: none; border-radius: 6px; } .db-video-card:hover .db-thumb::after, .db-image-card:hover .db-thumb::after { background: rgba(15, 23, 42, 0.55); color: #fff; } `); function showToast(msg, duration) { let toast = document.getElementById('db-dl-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'db-dl-toast'; document.body.appendChild(toast); } toast.textContent = msg; toast.classList.add('show'); clearTimeout(toast._timer); toast._timer = setTimeout(() => toast.classList.remove('show'), duration || 1800); } function normalizeKey(url) { try { const u = new URL(url); return u.origin + u.pathname; } catch (e) { return url.split('?')[0].split('#')[0]; } } function isVideoUrl(url) { if (!url) return false; if (url.includes('.mp4') || url.includes('.m3u8') || url.includes('.flv') || url.includes('.webm')) return true; if (url.includes('mime_type=video')) return true; if (url.includes('/video/') && url.includes('douyin')) return true; return false; } function isImageUrl(url) { if (!url) return false; if (/\.(png|jpg|jpeg|webp|gif|bmp|svg)(\?|$|#)/i.test(url)) return true; if (url.includes('mime_type=image')) return true; if (url.includes('imagex') || url.includes('rc_gen_image')) return true; if (url.includes('p1-sign') || url.includes('p3-flow-imagex') || url.includes('byteimg.com')) return true; return false; } function extractVideoSrc(element) { if (!element) return null; if (element.tagName === 'VIDEO') { if (element.src && element.src.startsWith('http')) return element.src; if (element.currentSrc && element.currentSrc.startsWith('http')) return element.currentSrc; const source = element.querySelector('source[src]'); if (source && source.src.startsWith('http')) return source.src; } if (element.querySelectorAll) { const innerVid = element.querySelector('video[src]'); if (innerVid && innerVid.src && innerVid.src.startsWith('http')) return innerVid.src; } const dataUrl = element.getAttribute && element.getAttribute('data-url'); if (dataUrl && dataUrl.startsWith('http')) return dataUrl; return null; } function extractImageSrc(element) { if (!element) return null; if (element.tagName === 'IMG') { const src = element.src || element.currentSrc; if (src && src.startsWith('http')) return src; const dataSrc = element.getAttribute('data-src') || element.getAttribute('data-original'); if (dataSrc && dataSrc.startsWith('http')) return dataSrc; } if (element.tagName === 'SOURCE') { if (element.src && element.src.startsWith('http')) return element.src; const srcset = element.srcset; if (srcset) { const firstUrl = srcset.split(',')[0].trim().split(' ')[0]; if (firstUrl.startsWith('http')) return firstUrl; } } if (element.querySelectorAll) { const innerImg = element.querySelector('img[src]'); if (innerImg) { const src = innerImg.src || innerImg.currentSrc; if (src && src.startsWith('http')) return src; } } const bgImg = element.style && element.style.backgroundImage; if (bgImg && bgImg.includes('url(')) { const match = bgImg.match(/url\(["']?(https?:\/\/[^"')]+)["']?\)/); if (match) return match[1]; } return null; } function getActiveMap() { return activeTab === 'video' ? videoLinks : imageLinks; } function getActiveProcessed() { return activeTab === 'video' ? processedVideoSrcs : processedImageSrcs; } function getActiveExtractor() { return activeTab === 'video' ? extractVideoSrc : extractImageSrc; } function processVideo(el) { const src = extractVideoSrc(el); if (!src) return; const cleanSrc = src.replace(/&/g, '&'); const key = normalizeKey(cleanSrc); if (processedVideoSrcs.has(key)) return; processedVideoSrcs.add(key); const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }); const filename = extractFilename(cleanSrc, '.mp4'); const resolution = extractResolution(cleanSrc); videoLinks.set(key, { src: cleanSrc, type: 'video', title: filename, timestamp, resolution }); if (activeTab === 'video') addCard(key, 'video'); updateBadge(); } function processImage(src, sizeKb) { if (!src || !src.startsWith('http')) return; if (!isImageUrl(src)) return; const cleanSrc = src.replace(/&/g, '&'); const key = normalizeKey(cleanSrc); if (processedImageSrcs.has(key)) return; processedImageSrcs.add(key); const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }); const filename = extractFilename(cleanSrc, '.png'); imageLinks.set(key, { src: cleanSrc, type: 'image', title: filename, timestamp, sizeKb }); if (activeTab === 'image') addCard(key, 'image'); updateBadge(); } function processElement(el) { const url = extractImageSrc(el); if (url && isImageUrl(url)) { processImage(url); return; } const vidUrl = extractVideoSrc(el); if (vidUrl) processVideo(el); } function extractFilename(url, defaultExt) { try { const u = new URL(url); const pathParts = u.pathname.split('/').filter(Boolean); if (pathParts.length > 0) { const last = pathParts[pathParts.length - 1]; if (last.includes('.')) return last; return last + defaultExt; } } catch (e) {} return 'file_' + Date.now() + defaultExt; } function extractResolution(url) { const match = url.match(/br=(\d+)/); if (match) { const br = parseInt(match[1]); return br > 1000 ? (br / 1000).toFixed(1) + ' Mbps' : br + ' kbps'; } return ''; } function formatSize(kb) { if (!kb) return ''; if (kb > 1024) return (kb / 1024).toFixed(1) + ' MB'; return kb + ' KB'; } function scanPage() { document.querySelectorAll('div[class*="video-canvas-panel-player"]').forEach(processVideo); document.querySelectorAll('.xgplayer').forEach(processVideo); document.querySelectorAll('video').forEach(processVideo); document.querySelectorAll('img[src]').forEach(el => { const src = el.src || el.currentSrc; if (src && src.startsWith('http')) processImage(src); }); document.querySelectorAll('img[data-src]').forEach(el => { const src = el.getAttribute('data-src'); if (src && src.startsWith('http')) processImage(src); }); document.querySelectorAll('source[srcset]').forEach(el => { const srcset = el.srcset; if (srcset) { const firstUrl = srcset.split(',')[0].trim().split(' ')[0]; if (firstUrl.startsWith('http')) processImage(firstUrl); } }); } function setupPerformanceObserver() { if (!window.PerformanceObserver) return; try { const obs = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { const url = entry.name; if (!url || !url.startsWith('http')) continue; if (!isImageUrl(url)) continue; const size = entry.transferSize ? Math.round(entry.transferSize / 1024) : 0; processImage(url, size); } }); obs.observe({ type: 'resource', buffered: true }); } catch (e) {} } function setupObserver() { const observer = new MutationObserver((mutations) => { let shouldScan = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.querySelectorAll) { const matches = node.querySelectorAll('video[src], img[src], img[data-src], .xgplayer, div[class*="video-canvas-panel-player"]'); if (matches.length > 0) { shouldScan = true; break; } } if (node.tagName === 'VIDEO' || node.tagName === 'IMG' || node.tagName === 'SOURCE') { shouldScan = true; break; } } } } if (mutation.type === 'attributes') { if (mutation.target.tagName === 'VIDEO' && mutation.attributeName === 'src') { if (mutation.target.src && mutation.target.src.startsWith('http')) processVideo(mutation.target); } if (mutation.target.tagName === 'IMG' && (mutation.attributeName === 'src' || mutation.attributeName === 'data-src')) { const src = mutation.target.src || mutation.target.currentSrc || mutation.target.getAttribute('data-src'); if (src && src.startsWith('http')) processImage(src); } } } if (shouldScan) { clearTimeout(observer._debounce); observer._debounce = setTimeout(scanPage, 500); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'data-src'] }); return observer; } function buildUI() { const btn = document.createElement('button'); btn.id = 'db-dl-btn'; btn.title = '豆包视频&图片下载助手'; btn.innerHTML = ` `; btn.addEventListener('click', togglePanel); setupDrag(btn); restorePosition(btn); document.body.appendChild(btn); const overlay = document.createElement('div'); overlay.id = 'db-dl-overlay'; overlay.addEventListener('click', closePanel); document.body.appendChild(overlay); const panel = document.createElement('div'); panel.id = 'db-dl-panel'; panel.innerHTML = `
豆包视频&图片下载助手
免责声明:本脚本仅供学习和参考,切勿用于违法或商用,否则后果自负。
`; document.body.appendChild(panel); document.getElementById('db-close-btn').addEventListener('click', closePanel); document.getElementById('db-scan-btn').addEventListener('click', () => { scanPage(); showToast('扫描完成'); }); document.getElementById('db-copyall-btn').addEventListener('click', copyAllLinks); document.getElementById('db-dlall-btn').addEventListener('click', downloadAll); document.getElementById('db-tab-video').addEventListener('click', () => switchTab('video')); document.getElementById('db-tab-image').addEventListener('click', () => switchTab('image')); renderActiveTab(); } function switchTab(tab) { activeTab = tab; document.getElementById('db-tab-video').classList.toggle('active', tab === 'video'); document.getElementById('db-tab-image').classList.toggle('active', tab === 'image'); renderActiveTab(); } function renderActiveTab() { const container = document.getElementById('db-list-container'); if (!container) return; container.innerHTML = ''; const items = Array.from(getActiveMap().values()); if (items.length === 0) { container.innerHTML = `
暂无检测到${activeTab === 'video' ? '视频' : '图片'} ${activeTab === 'video' ? '播放页面中的视频后将自动捕获' : '浏览页面或等待图片加载完成'}
`; return; } items.forEach(item => addCard(null, item.type, item)); } function addCard(key, type, preloadedItem) { const container = document.getElementById('db-list-container'); if (!container) return; const emptyEl = container.querySelector('.db-empty-state'); if (emptyEl) emptyEl.remove(); const map = type === 'video' ? videoLinks : imageLinks; const item = preloadedItem || map.get(key); if (!item) return; const card = document.createElement('div'); card.className = type === 'video' ? 'db-video-card' : 'db-image-card'; card.dataset.src = item.src; card.dataset.key = key; const metaParts = [`${type === 'video' ? 'MP4' : 'IMG'}`, `${item.timestamp}`]; if (type === 'video' && item.resolution) metaParts.push(`${item.resolution}`); if (type === 'image' && item.sizeKb) metaParts.push(`${formatSize(item.sizeKb)}`); card.innerHTML = `
${metaParts.join('')}
${item.title}
`; card.querySelector('.copy-btn').addEventListener('click', function () { copyToClipboard(item.src, this); }); card.querySelector('.dl-btn').addEventListener('click', function () { downloadFile(item.src, item.title); }); card.querySelector('.db-delete-btn').addEventListener('click', function (e) { e.stopPropagation(); deleteCard(key, type, card); }); card.addEventListener('click', function (e) { if (e.target.closest('.db-card-actions') || e.target.closest('.db-delete-btn')) return; openPreview(item.src, type, item.src); }); loadThumb(card.querySelector('.db-thumb'), item.src, type); container.insertBefore(card, container.firstChild); updateCounts(); } function deleteCard(key, type, cardEl) { if (!cardEl) return; cardEl.style.animation = 'db-card-out 0.3s ease forwards'; setTimeout(() => { cardEl.remove(); if (type === 'video') { videoLinks.delete(key); processedVideoSrcs.delete(key); } else { imageLinks.delete(key); processedImageSrcs.delete(key); } updateCounts(); updateBadge(); const container = document.getElementById('db-list-container'); if (container && container.children.length === 0) renderActiveTab(); showToast('已删除'); }, 280); } function loadThumb(thumbEl, src, type) { if (type === 'image') { const img = document.createElement('img'); img.crossOrigin = 'anonymous'; img.referrerPolicy = 'no-referrer'; img.onload = () => { thumbEl.innerHTML = ''; thumbEl.appendChild(img); }; img.onerror = () => {}; img.src = src; } else { // 视频:先用 canvas 截取第一帧 const video = document.createElement('video'); video.src = src; video.crossOrigin = 'anonymous'; video.preload = 'metadata'; video.muted = true; video.style.cssText = 'width:100%;height:100%;object-fit:cover;'; let thumbLoaded = false; video.addEventListener('loadeddata', () => { try { video.currentTime = Math.min(0.1, video.duration * 0.05); } catch (e) {} }); video.addEventListener('seeked', () => { if (thumbLoaded) return; thumbLoaded = true; try { const canvas = document.createElement('canvas'); canvas.width = 160; canvas.height = 90; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const img = document.createElement('img'); img.src = canvas.toDataURL('image/jpeg', 0.7); img.style.cssText = 'width:100%;height:100%;object-fit:cover;'; thumbEl.innerHTML = ''; thumbEl.appendChild(img); } catch (e) {} cleanup(); }); video.addEventListener('error', () => { if (thumbLoaded) return; thumbLoaded = true; cleanup(); }); function cleanup() { try { video.pause(); video.removeAttribute('src'); video.load(); } catch (e) {} } // 兜底:2秒后还没加载就用 img 标签加载视频 URL(部分浏览器支持) setTimeout(() => { if (thumbLoaded) return; thumbLoaded = true; const img = document.createElement('img'); img.crossOrigin = 'anonymous'; img.referrerPolicy = 'no-referrer'; img.style.cssText = 'width:100%;height:100%;object-fit:cover;'; img.onload = () => { if (img.naturalWidth > 0) { thumbEl.innerHTML = ''; thumbEl.appendChild(img); } }; img.src = src; }, 2500); } } function openPreview(src, type, title) { let modal = document.getElementById('db-preview-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'db-preview-modal'; modal.innerHTML = `
`; document.body.appendChild(modal); modal.addEventListener('click', (e) => { if (e.target === modal) closePreview(); }); modal.querySelector('.preview-close').addEventListener('click', closePreview); } const content = modal.querySelector('.preview-content'); const info = modal.querySelector('.preview-info'); content.innerHTML = ''; info.textContent = title || ''; if (type === 'video') { const video = document.createElement('video'); video.src = src; video.controls = true; video.autoplay = true; video.style.cssText = 'max-width:92vw;max-height:88vh;'; content.appendChild(video); } else { const img = document.createElement('img'); img.src = src; img.alt = title || ''; content.appendChild(img); } modal.classList.add('show'); } function closePreview() { const modal = document.getElementById('db-preview-modal'); if (!modal) return; modal.classList.remove('show'); const content = modal.querySelector('.preview-content'); if (content) { content.querySelectorAll('video').forEach(v => { try { v.pause(); v.removeAttribute('src'); v.load(); } catch (e) {} }); content.innerHTML = ''; } } function updateCounts() { const vc = videoLinks.size; const ic = imageLinks.size; document.getElementById('db-tab-video-count').textContent = vc; document.getElementById('db-tab-image-count').textContent = ic; } function updateBadge() { const badge = document.querySelector('#db-dl-btn .badge'); if (!badge) return; const total = videoLinks.size + imageLinks.size; if (total === 0) { badge.classList.add('hidden'); badge.classList.remove('pulse'); } else { badge.classList.remove('hidden'); badge.style.display = 'block'; badge.classList.remove('pulse'); void badge.offsetWidth; badge.classList.add('pulse'); } } function togglePanel() { const panel = document.getElementById('db-dl-panel'); if (panel.classList.contains('open')) closePanel(); else openPanel(); } function openPanel() { document.getElementById('db-dl-panel').classList.add('open'); document.getElementById('db-dl-overlay').classList.add('show'); document.body.style.overflow = 'hidden'; renderActiveTab(); updateCounts(); } function closePanel() { document.getElementById('db-dl-panel').classList.remove('open'); document.getElementById('db-dl-overlay').classList.remove('show'); document.body.style.overflow = ''; } async function copyToClipboard(text, btnEl) { const originalHTML = btnEl.innerHTML; try { await navigator.clipboard.writeText(text); if (btnEl) { btnEl.classList.add('copied'); btnEl.textContent = '✓ 已复制'; setTimeout(() => { btnEl.classList.remove('copied'); btnEl.innerHTML = originalHTML; }, 1800); } showToast('链接已复制'); } catch (e) { const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;left:-9999px;'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); showToast('链接已复制'); } } function copyAllLinks() { const map = getActiveMap(); if (map.size === 0) { showToast('没有可复制的链接'); return; } const urls = Array.from(map.values()).map(v => v.src).join('\n'); copyToClipboard(urls, null); showToast(`已复制 ${map.size} 个${activeTab === 'video' ? '视频' : '图片'}链接`); } function downloadFile(src, filename) { const a = document.createElement('a'); a.href = src; a.download = filename; a.target = '_blank'; a.rel = 'noopener noreferrer'; document.body.appendChild(a); a.click(); setTimeout(() => document.body.removeChild(a), 100); setTimeout(() => { try { window.open(src, '_blank', 'noopener,noreferrer'); } catch (e) {} }, 500); showToast('开始下载'); } function downloadAll() { const map = getActiveMap(); if (map.size === 0) { showToast('没有可下载的资源'); return; } const links = Array.from(map.values()).map(v => ({ src: v.src, title: v.title })); showToast(`开始下载 ${links.length} 个${activeTab === 'video' ? '视频' : '图片'}`); links.forEach((item, i) => setTimeout(() => downloadFile(item.src, item.title), i * 800)); } function setupDrag(btn) { let isDragging = false; let startX, startY, startRight, startBottom; let moved = false; const onStart = (e) => { if (e.button !== 0) return; isDragging = true; moved = false; startX = e.clientX; startY = e.clientY; const rect = btn.getBoundingClientRect(); startRight = window.innerWidth - rect.right; startBottom = window.innerHeight - rect.bottom; btn.style.transition = 'none'; document.body.style.userSelect = 'none'; }; const onMove = (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) moved = true; const newRight = Math.max(0, Math.min(window.innerWidth - 48, startRight - dx)); const newBottom = Math.max(0, Math.min(window.innerHeight - 48, startBottom - dy)); btn.style.right = newRight + 'px'; btn.style.bottom = newBottom + 'px'; }; const onEnd = () => { if (!isDragging) return; isDragging = false; btn.style.transition = ''; document.body.style.userSelect = ''; if (moved) { savePosition(btn); btn._suppressClick = true; setTimeout(() => { btn._suppressClick = false; }, 50); } }; btn.addEventListener('mousedown', onStart); document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onEnd); btn.addEventListener('click', (e) => { if (btn._suppressClick) { e.stopImmediatePropagation(); e.preventDefault(); } }, true); btn.addEventListener('touchstart', (e) => { const t = e.touches[0]; onStart({ button: 0, clientX: t.clientX, clientY: t.clientY }); }, { passive: true }); btn.addEventListener('touchmove', (e) => { const t = e.touches[0]; onMove({ clientX: t.clientX, clientY: t.clientY }); }, { passive: true }); btn.addEventListener('touchend', onEnd); } function savePosition(btn) { try { const rect = btn.getBoundingClientRect(); localStorage.setItem('db-dl-btn-pos', JSON.stringify({ right: window.innerWidth - rect.right, bottom: window.innerHeight - rect.bottom })); } catch (e) {} } function restorePosition(btn) { try { const saved = localStorage.getItem('db-dl-btn-pos'); if (saved) { const pos = JSON.parse(saved); if (typeof pos.right === 'number') btn.style.right = pos.right + 'px'; if (typeof pos.bottom === 'number') btn.style.bottom = pos.bottom + 'px'; } } catch (e) {} } function setupKeyboard() { document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.shiftKey && e.key === 'D') { e.preventDefault(); togglePanel(); } if (e.key === 'Escape') { const modal = document.getElementById('db-preview-modal'); if (modal && modal.classList.contains('show')) { closePreview(); return; } const panel = document.getElementById('db-dl-panel'); if (panel && panel.classList.contains('open')) closePanel(); } }); } function init() { buildUI(); setupKeyboard(); setTimeout(scanPage, 1500); setupPerformanceObserver(); setupObserver(); setInterval(scanPage, 2000); console.log('[豆包下载助手] v5.0.0 已启动,支持视频&图片,快捷键 Ctrl+Shift+D'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(init, 1000)); } else { setTimeout(init, 1000); } })();