// ==UserScript== // @name X 博主综合操作脚本(ZIP打包版) // @namespace http://tampermonkey.net/ // @version 2.0 // @description 支持两种方式:① 普通网页扫描下载(ZIP打包) ② API下载图片/视频(ZIP打包+新版接口),带进度提示与取消功能 // @author chatGPT + 整合自Twitter Media Downloader // @match https://x.com/* // @match https://twitter.com/* // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_download // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @downloadURL none // ==/UserScript== (function () { 'use strict'; // -------------------------- 基础配置与变量 -------------------------- const BATCH_SIZE = 1000; const DOWNLOAD_PAUSE = 1000; const scrollInterval = 3000; const maxScrollCount = 10000; const IMAGE_SCROLL_INTERVAL = 1500; const IMAGE_MAX_SCROLL_COUNT = 100; let cancelDownload = false; const mediaSet = new Set(); const imageSet = new Set(); const statusIdSet = new Set(); let hideTimeoutId = null; let lang, host, history, show_sensitive; const filenameTemplate = 'twitter_{user-name}(@{user-id})_{date-time}_{status-id}_{file-type}'; // -------------------------- UI组件初始化 -------------------------- // 进度框 const progressBox = document.createElement('div'); Object.assign(progressBox.style, { position: 'fixed', top: '20px', left: '20px', padding: '10px', backgroundColor: 'rgba(0,0,0,0.8)', color: '#fff', fontSize: '14px', zIndex: 9999, borderRadius: '8px', display: 'none' }); document.body.appendChild(progressBox); // 加载提示框 const loadingPrompt = document.createElement('div'); Object.assign(loadingPrompt.style, { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', padding: '20px', backgroundColor: 'rgba(0,0,0,0.8)', color: '#fff', fontSize: '16px', zIndex: 10000, borderRadius: '8px', display: 'none' }); loadingPrompt.textContent = '正在加载,请不要关闭页面...'; document.body.appendChild(loadingPrompt); // 进度条 const progressBarContainer = document.createElement('div'); Object.assign(progressBarContainer.style, { position: 'fixed', top: '55%', left: '50%', transform: 'translateX(-50%)', width: '300px', height: '20px', backgroundColor: '#ccc', zIndex: 10000, borderRadius: '10px', display: 'none' }); const progressBar = document.createElement('div'); Object.assign(progressBar.style, { width: '0%', height: '100%', backgroundColor: '#1DA1F2', borderRadius: '10px' }); progressBarContainer.appendChild(progressBar); document.body.appendChild(progressBarContainer); // 下载状态通知器(来自指定脚本) const notifier = document.createElement('div'); Object.assign(notifier.style, { display: 'none', position: 'fixed', left: '16px', bottom: '16px', color: '#000', background: '#fff', border: '1px solid #ccc', borderRadius: '8px', padding: '4px' }); notifier.title = 'X Media Downloader'; notifier.className = 'tmd-notifier'; notifier.innerHTML = '|'; document.body.appendChild(notifier); // -------------------------- 工具函数 -------------------------- // 更新进度提示 function updateProgress(txt) { progressBox.innerText = txt; progressBox.style.display = 'block'; } // 获取用户名 function getUsername() { const m = window.location.pathname.match(/^\/([^\/\?]+)/); return m ? m[1] : 'unknown_user'; } // 初始化基础配置(来自指定脚本) async function initBaseConfig() { lang = getLanguage(); host = location.hostname; history = await getDownloadHistory(); show_sensitive = GM_getValue('show_sensitive', false); // 注入样式 document.head.insertAdjacentHTML('beforeend', ``); } // -------------------------- 核心逻辑整合(来自指定脚本) -------------------------- // 1. 语言配置 function getLanguage() { const langMap = { en: { download: 'Download', completed: 'Download Completed', settings: 'Settings', dialog: { title: 'Download Settings', save: 'Save', save_history: 'Remember download history', clear_history: '(Clear)', clear_confirm: 'Clear download history?', show_sensitive: 'Always show sensitive content', pattern: 'File Name Pattern' }, enable_packaging: 'Package multiple files into a ZIP', packaging: 'Packaging ZIP...', mediaNotFound: 'MEDIA_NOT_FOUND', linkNotSupported: 'This tweet contains a link, which is not supported' }, zh: { download: '下载', completed: '下载完成', settings: '设置', dialog: { title: '下载设置', save: '保存', save_history: '保存下载记录', clear_history: '(清除)', clear_confirm: '确认要清除下载记录?', show_sensitive: '自动显示敏感内容', pattern: '文件名格式' }, enable_packaging: '多文件打包成 ZIP', packaging: '正在打包ZIP...', mediaNotFound: '未找到媒体文件', linkNotSupported: '此推文包含链接,暂不支持下载' }, 'zh-Hant': { download: '下載', completed: '下載完成', settings: '設置', dialog: { title: '下載設置', save: '保存', save_history: '保存下載記錄', clear_history: '(清除)', clear_confirm: '確認要清除下載記錄?', show_sensitive: '自動顯示敏感內容', pattern: '文件名規則' }, enable_packaging: '多文件打包成 ZIP', packaging: '正在打包ZIP...', mediaNotFound: '未找到媒體文件', linkNotSupported: '此推文包含鏈接,暫不支持下載' } }; const pageLang = document.querySelector('html').lang || navigator.language; return langMap[pageLang] || langMap[pageLang.split('-')[0]] || langMap.en; } // 2. CSS样式(来自指定脚本) function getCSS() { return ` .tmd-notifier.running {display: flex; align-items: center;} .tmd-notifier label {display: inline-flex; align-items: center; margin: 0 8px;} .tmd-notifier label:before {content: " "; width: 32px; height: 16px; background-position: center; background-repeat: no-repeat;} .tmd-notifier label:nth-child(1):before {background-image:url("data:image/svg+xml;charset=utf8,");} .tmd-notifier label:nth-child(2):before {background-image:url("data:image/svg+xml;charset=utf8,");} .tmd-notifier label:nth-child(3):before {background-image:url("data:image/svg+xml;charset=utf8,");} .tmd-btn {display: inline-block; background-color: #1DA1F2; color: #FFFFFF; padding: 0 15px; border-radius: 99px; cursor: pointer; margin-left: 10px;} .tmd-btn:hover {background-color: rgba(29, 161, 242, 0.9);} .tmd-tag {display: inline-block; background-color: #FFFFFF; color: #1DA1F2; padding: 0 10px; border-radius: 10px; border: 1px solid #1DA1F2; font-weight: bold; margin: 5px; cursor: pointer;} .tmd-tag:hover {background-color: rgba(29, 161, 242, 0.1);} `; } // 敏感内容显示CSS(来自指定脚本) function getSensitiveCSS() { return ` li[role="listitem"]>div>div>div>div:not(:last-child) {filter: none;} li[role="listitem"]>div>div>div>div+div:last-child {display: none;} `; } // 3. 下载历史管理(来自指定脚本) async function getDownloadHistory() { let history = await GM_getValue('download_history', []); // 兼容旧版localStorage存储 const oldHistory = JSON.parse(localStorage.getItem('history') || '[]'); if (oldHistory.length > 0) { history = [...new Set([...history, ...oldHistory])]; GM_setValue('download_history', history); localStorage.removeItem('history'); } return history; } // 保存下载历史 async function saveDownloadHistory(statusId) { const history = await getDownloadHistory(); if (!history.includes(statusId)) { history.push(statusId); GM_setValue('download_history', history); } } // 4. 日期格式化(来自指定脚本) function formatDate(dateStr, format = 'YYYYMMDD-hhmmss', useLocal = false) { const d = new Date(dateStr); if (useLocal) d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); const months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']; const values = { YYYY: d.getUTCFullYear().toString(), YY: d.getUTCFullYear().toString().slice(-2), MM: (d.getUTCMonth() + 1).toString().padStart(2, '0'), MMM: months[d.getUTCMonth()], DD: d.getUTCDate().toString().padStart(2, '0'), hh: d.getUTCHours().toString().padStart(2, '0'), mm: d.getUTCMinutes().toString().padStart(2, '0'), ss: d.getUTCSeconds().toString().padStart(2, '0') }; return format.replace(/(YYYY|YY|MM|MMM|DD|hh|mm|ss)/g, match => values[match]); } // 5. 文件名生成(来自指定脚本) function generateFileName(media, tweetData, index = 0) { const invalidChars = { '\\': '\', '/': '/', '|': '|', '<': '<', '>': '>', ':': ':', '*': '*', '?': '?', '"': '"', '\u200b': '', '\u200c': '', '\u200d': '', '\u2060': '', '\ufeff': '', '🔞': '' }; const user = tweetData.core.user_results.result.legacy; const tweet = tweetData.legacy; // 基础信息 const info = { 'user-name': user.name.replace(/([\\/|*?:"\u200b-\u200d\u2060\ufeff]|🔞)/g, v => invalidChars[v]), 'user-id': user.screen_name, 'status-id': tweet.id_str, 'date-time': formatDate(tweet.created_at), 'date-time-local': formatDate(tweet.created_at, 'YYYYMMDD-hhmmss', true), 'file-type': media.type.replace('animated_', ''), 'file-name': media.media_url_https.split('/').pop().split(':')[0], 'file-ext': media.type === 'photo' ? 'jpg' : 'mp4' }; // 获取用户自定义模板 const template = GM_getValue('filename', filenameTemplate); // 替换模板变量 let fileName = template.replace(/\{([^{}:]+)\}/g, (_, key) => info[key] || key); // 多文件时添加索引 if (index > 0) fileName += `_${index}`; // 补充后缀 if (!fileName.endsWith(`.${info['file-ext']}`)) fileName += `.${info['file-ext']}`; return fileName; } // 6. 新版API请求(来自指定脚本,替换原有fetchJson) async function fetchTweetData(statusId) { const baseUrl = `https://${host}/i/api/graphql/2ICDjqPd81tulZcYrtpTuQ/TweetResultByRestId`; const variables = { 'tweetId': statusId, 'with_rux_injections': false, 'includePromotedContent': true, 'withCommunity': true, 'withQuickPromoteEligibilityTweetFields': true, 'withBirdwatchNotes': true, 'withVoice': true, 'withV2Timeline': true }; const features = { 'articles_preview_enabled': true, 'c9s_tweet_anatomy_moderator_badge_enabled': true, 'communities_web_enable_tweet_community_results_fetch': false, 'creator_subscriptions_quote_tweet_preview_enabled': false, 'creator_subscriptions_tweet_preview_api_enabled': false, 'freedom_of_speech_not_reach_fetch_enabled': true, 'graphql_is_translatable_rweb_tweet_is_translatable_enabled': true, 'longform_notetweets_consumption_enabled': false, 'longform_notetweets_inline_media_enabled': true, 'longform_notetweets_rich_text_read_enabled': false, 'premium_content_api_read_enabled': false, 'profile_label_improvements_pcf_label_in_post_enabled': true, 'responsive_web_edit_tweet_api_enabled': false, 'responsive_web_enhance_cards_enabled': false, 'responsive_web_graphql_exclude_directive_enabled': false, 'responsive_web_graphql_skip_user_profile_image_extensions_enabled': false, 'responsive_web_graphql_timeline_navigation_enabled': false, 'responsive_web_media_download_video_enabled': false, 'responsive_web_twitter_article_tweet_consumption_enabled': true, 'standardized_nudges_misinfo': true, 'tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled': true, 'verified_phone_label_enabled': false, 'view_counts_everywhere_api_enabled': true }; // 构建请求URL const url = encodeURI(`${baseUrl}?variables=${JSON.stringify(variables)}&features=${JSON.stringify(features)}`); // 获取Cookie const cookies = getCookies(); // 请求头 const headers = { 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', 'x-twitter-active-user': 'yes', 'x-twitter-client-language': cookies.lang || 'en', 'x-csrf-token': cookies.ct0 || '' }; // Guest Token(必要时添加) if (cookies.ct0?.length === 32 && cookies.gt) headers['x-guest-token'] = cookies.gt; try { const response = await fetch(url, { headers }); const data = await response.json(); const tweetResult = data.data.tweetResult.result; return tweetResult.tweet || tweetResult; } catch (error) { console.error(`获取推文${statusId}数据失败:`, error); throw new Error(`获取推文数据失败:${error.message}`); } } // 获取Cookie(来自指定脚本) function getCookies(name) { const cookies = {}; document.cookie.split(';') .filter(item => item.includes('=')) .forEach(item => { const [key, value] = item.trim().split('='); cookies[key] = value; }); return name ? cookies[name] : cookies; } // 7. ZIP打包下载器(整合指定脚本逻辑) const Downloader = (() => { let tasks = [], thread = 0, failed = 0, hasFailed = false; return { // 添加下载任务 async add(tasksList, sourceBtn) { if (cancelDownload) return; // 获取用户设置:是否打包 const enablePackaging = GM_getValue('enable_packaging', true); const saveHistory = GM_getValue('save_history', true); // 单文件直接下载,多文件打包 if (tasksList.length === 1 && !enablePackaging) { this.downloadSingle(tasksList[0], sourceBtn, saveHistory); } else { this.downloadZip(tasksList, sourceBtn, saveHistory); } }, // 单文件下载 async downloadSingle(task, sourceBtn, saveHistory) { thread++; this.updateNotifier(); updateProgress(`正在下载:${task.name}`); try { await new Promise((resolve, reject) => { GM_download({ url: task.url, name: task.name, onload: () => { thread--; failed--; tasks = tasks.filter(t => t.url !== task.url); this.updateNotifier(); updateProgress(`✅ 下载完成:${task.name}`); if (saveHistory) saveDownloadHistory(task.statusId); resolve(); }, onerror: (result) => { thread--; failed++; tasks = tasks.filter(t => t.url !== task.url); this.updateNotifier(); updateProgress(`❌ 下载失败:${task.name}(${result.details})`); reject(new Error(result.details)); } }); }); } catch (error) { console.error('单文件下载失败:', error); } }, // 多文件ZIP打包下载 async downloadZip(tasksList, sourceBtn, saveHistory) { if (cancelDownload) return; const zip = new JSZip(); let completedCount = 0; const total = tasksList.length; updateProgress(`${lang.packaging}(0/${total})`); // 添加所有任务到队列 tasks.push(...tasksList); this.updateNotifier(); try { // 并行下载文件并添加到ZIP await Promise.all(tasksList.map(async (task, index) => { thread++; this.updateNotifier(); try { const response = await fetch(task.url); if (!response.ok) throw new Error(`HTTP错误:${response.status}`); const blob = await response.blob(); zip.file(task.name, blob); // 更新进度 completedCount++; updateProgress(`${lang.packaging}(${completedCount}/${total})`); } catch (error) { failed++; updateProgress(`❌ 文件${task.name}下载失败:${error.message}`); console.error(`文件${task.name}处理失败:`, error); } finally { thread--; tasks = tasks.filter(t => t.url !== task.url); this.updateNotifier(); } })); // 生成ZIP并下载 if (cancelDownload) return; updateProgress('正在生成ZIP文件...'); const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'STORE' // 媒体文件压缩无效,用存储模式提速 }); // 下载ZIP const zipName = `${tasksList[0].name.split('_status-')[0]}_batch_${total}files.zip`; const a = document.createElement('a'); a.href = URL.createObjectURL(zipBlob); a.download = zipName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); // 保存历史(去重) if (saveHistory) { const statusIds = [...new Set(tasksList.map(task => task.statusId))]; statusIds.forEach(id => saveDownloadHistory(id)); } updateProgress(`✅ ZIP打包完成:${zipName}(共${total}个文件)`); } catch (error) { updateProgress(`❌ ZIP打包失败:${error.message}`); console.error('ZIP打包错误:', error); } }, // 更新下载状态通知器 updateNotifier() { if (failed > 0 && !hasFailed) { hasFailed = true; notifier.innerHTML += '|'; const clearBtn = document.createElement('label'); clearBtn.innerText = '清空失败'; clearBtn.style.color = '#f33'; clearBtn.onclick = () => { failed = 0; hasFailed = false; notifier.innerHTML = '|'; this.updateNotifier(); }; notifier.appendChild(clearBtn); } // 更新数值 notifier.children[0].innerText = thread; // 正在下载 notifier.children[1].innerText = tasks.length - thread - failed; // 等待中 if (failed > 0) notifier.children[2].innerText = failed; // 失败数 // 显示/隐藏通知器 if (thread > 0 || tasks.length > 0 || failed > 0) { notifier.classList.add('running'); } else { notifier.classList.remove('running'); } }, // 取消所有任务 cancel() { cancelDownload = true; tasks = []; thread = 0; failed = 0; hasFailed = false; this.updateNotifier(); updateProgress('⏹️ 下载已取消'); } }; })(); // -------------------------- 原有功能重构 -------------------------- // 1. API下载:重构为新版逻辑(收集statusId → 获取媒体 → 打包下载) async function processTweets() { const statusIds = []; // 收集页面中的statusId(去重) document.querySelectorAll('a[href*="/status/"]').forEach(link => { const statusId = link.href.split('/status/').pop().split('/').shift(); if (statusId && !statusIdSet.has(statusId) && /^\d+$/.test(statusId)) { statusIds.push(statusId); statusIdSet.add(statusId); } }); if (statusIds.length === 0) return []; updateProgress(`正在解析${statusIds.length}条推文的媒体...`); // 批量获取推文媒体信息 const mediaTasks = []; for (const statusId of statusIds) { if (cancelDownload) break; try { const tweetData = await fetchTweetData(statusId); const tweet = tweetData.legacy; const medias = tweet.extended_entities?.media; // 跳过包含链接卡片的推文 if (tweetData.card) { console.warn(`推文${statusId}包含链接卡片,跳过`); continue; } // 提取媒体URL if (Array.isArray(medias)) { medias.forEach((media, index) => { let mediaUrl; if (media.type === 'photo') { mediaUrl = `${media.media_url_https}:orig`; // 原图 } else if (media.type === 'video' || media.type === 'animated_gif') { // 选最高码率MP4 const variants = media.video_info?.variants.filter(v => v.content_type === 'video/mp4'); if (variants && variants.length > 0) { mediaUrl = variants.sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0].url; } } if (mediaUrl && !mediaSet.has(mediaUrl)) { mediaSet.add(mediaUrl); // 生成任务:包含URL、文件名、所属statusId mediaTasks.push({ url: mediaUrl.split('?')[0], // 去除参数 name: generateFileName(media, tweetData, index), statusId: statusId }); } }); } } catch (error) { console.error(`处理推文${statusId}失败:`, error); continue; } } // 更新进度条(按已处理statusId比例) const progress = Math.min(Math.floor((statusIdSet.size / BATCH_SIZE) * 100), 100); progressBar.style.width = `${progress}%`; return mediaTasks; } // API下载主流程 async function autoScrollAndDownloadAPI() { cancelDownload = false; mediaSet.clear(); statusIdSet.clear(); Downloader.updateNotifier(); // 初始化UI updateProgress('📦 正在收集推文...'); loadingPrompt.style.display = 'block'; progressBarContainer.style.display = 'block'; progressBar.style.width = '0%'; // 初始扫描 let mediaTasks = await processTweets(); updateProgress(`📷 已扫描${mediaSet.size}个媒体文件`); // 自动滚动加载更多 let scrollCount = 0, lastHeight = 0, stableCount = 0; while (!cancelDownload && scrollCount < maxScrollCount && mediaSet.size < BATCH_SIZE && stableCount < 3) { // 滚动到底部 window.scrollTo(0, document.body.scrollHeight); await new Promise(resolve => setTimeout(resolve, scrollInterval)); // 重新扫描 const newTasks = await processTweets(); mediaTasks = [...mediaTasks, ...newTasks]; // 检测是否滚动到底(高度不变) const currentHeight = document.body.scrollHeight; if (currentHeight === lastHeight) { stableCount++; } else { stableCount = 0; lastHeight = currentHeight; } scrollCount++; updateProgress(`📷 已扫描${mediaSet.size}个媒体文件(滚动${scrollCount}次)`); } // 关闭加载提示 loadingPrompt.style.display = 'none'; progressBarContainer.style.display = 'none'; // 执行下载 if (cancelDownload) { updateProgress('⏹️ API下载已取消'); finishAndSave(apiStartBtn); return; } if (mediaTasks.length === 0) { updateProgress(`⚠️ 未找到可下载的媒体文件`); finishAndSave(apiStartBtn); return; } // 限制批量大小 const finalTasks = mediaTasks.slice(0, BATCH_SIZE); updateProgress(`🚀 开始处理${finalTasks.length}个媒体文件`); await Downloader.add(finalTasks, apiStartBtn); // 完成后清理 finishAndSave(apiStartBtn); } // 2. 普通图片下载:重构为ZIP打包 async function autoScrollAndDownloadImages() { cancelDownload = false; imageSet.clear(); const username = getUsername(); // 初始化UI updateProgress('📸 正在收集图片...'); progressBox.style.display = 'block'; // 初始扫描 getAllImages(); updateProgress(`📦 已找到${imageSet.size}张图片`); // 自动滚动加载 let scrollCount = 0, lastHeight = 0; while (scrollCount < IMAGE_MAX_SCROLL_COUNT && !cancelDownload) { window.scrollTo(0, document.body.scrollHeight); await new Promise(resolve => setTimeout(resolve, IMAGE_SCROLL_INTERVAL)); getAllImages(); const currentHeight = document.body.scrollHeight; updateProgress(`📦 已找到${imageSet.size}张图片(滚动${scrollCount+1}次)`); // 检测到底部 if (currentHeight === lastHeight) break; lastHeight = currentHeight; scrollCount++; } // 取消处理 if (cancelDownload) { updateProgress('⏹️ 普通下载已取消'); finishAndSave(startBtn); return; } // 生成下载任务 const imageList = Array.from(imageSet); if (imageList.length === 0) { updateProgress('⚠️ 未找到可下载的图片'); finishAndSave(startBtn); return; } // 构建任务列表 const tasks = imageList.map((url, index) => ({ url: url, name: `${username}_img_${formatDate(new Date())}_${index+1}.jpg`, statusId: `img_batch_${Date.now()}` // 图片批量标记 })); // 执行下载(打包) updateProgress(`🚀 开始下载${tasks.length}张图片`); await Downloader.add(tasks, startBtn); // 完成清理 finishAndSave(startBtn); } // 收集普通图片URL(原有逻辑保留) function getAllImages() { document.querySelectorAll('img[src*="twimg.com/media"], img[src*="pbs.twimg.com/amplify_video_thumb"]').forEach(img => { // 替换为原图URL const url = img.src.replace(/&name=\w+/, '') + '&name=orig'; imageSet.add(url); }); } // 下载完成后清理 function finishAndSave(btn) { btn.disabled = false; cancelBtn.style.display = 'none'; // 5秒后隐藏进度框 hideTimeoutId = setTimeout(() => { progressBox.style.display = 'none'; }, 5000); } // -------------------------- 设置面板(来自指定脚本) -------------------------- async function openSettings() { // 创建遮罩层 const mask = document.createElement('div'); Object.assign(mask.style, { position: 'fixed', left: 0, top: 0, width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.5)', zIndex: 10001, display: 'flex', alignItems: 'center', justifyContent: 'center' }); // 创建设置面板 const panel = document.createElement('div'); Object.assign(panel.style, { width: '600px', maxWidth: '90vw', backgroundColor: '#fff', borderRadius: '10px', padding: '20px', color: '#000' }); // 标题栏 const titleBar = document.createElement('div'); titleBar.style.display = 'flex'; titleBar.style.justifyContent = 'space-between'; titleBar.style.alignItems = 'center'; const title = document.createElement('h3'); title.innerText = lang.dialog.title; title.style.margin = 0; const closeBtn = document.createElement('button'); closeBtn.innerText = '×'; closeBtn.style.border = 'none'; closeBtn.style.background = 'none'; closeBtn.style.fontSize = '20px'; closeBtn.style.cursor = 'pointer'; closeBtn.style.color = '#666'; closeBtn.onclick = () => mask.remove(); titleBar.appendChild(title); titleBar.appendChild(closeBtn); panel.appendChild(titleBar); // 配置项区域 const configArea = document.createElement('div'); configArea.style.marginTop = '15px'; // 1. 保存下载记录 const historyConfig = document.createElement('div'); historyConfig.style.marginBottom = '15px'; historyConfig.style.padding = '10px'; historyConfig.style.border = '1px solid #eee'; historyConfig.style.borderRadius = '5px'; const historyLabel = document.createElement('label'); historyLabel.style.display = 'flex'; historyLabel.style.alignItems = 'center'; const historyCheckbox = document.createElement('input'); historyCheckbox.type = 'checkbox'; historyCheckbox.checked = GM_getValue('save_history', true); historyCheckbox.style.marginRight = '10px'; historyCheckbox.onchange = () => GM_setValue('save_history', historyCheckbox.checked); historyLabel.innerText = lang.dialog.save_history; historyLabel.insertBefore(historyCheckbox, historyLabel.firstChild); // 清除历史按钮 const clearBtn = document.createElement('span'); clearBtn.innerText = lang.dialog.clear_history; clearBtn.style.color = '#1DA1F2'; clearBtn.style.marginLeft = '15px'; clearBtn.style.cursor = 'pointer'; clearBtn.onclick = async () => { if (confirm(lang.dialog.clear_confirm)) { GM_setValue('download_history', []); history = []; alert(lang.dialog.clear_history + '成功'); } }; historyLabel.appendChild(clearBtn); historyConfig.appendChild(historyLabel); configArea.appendChild(historyConfig); // 2. 显示敏感内容 const sensitiveConfig = document.createElement('div'); sensitiveConfig.style.marginBottom = '15px'; sensitiveConfig.style.padding = '10px'; sensitiveConfig.style.border = '1px solid #eee'; sensitiveConfig.style.borderRadius = '5px'; const sensitiveLabel = document.createElement('label'); sensitiveLabel.style.display = 'flex'; sensitiveLabel.style.alignItems = 'center'; const sensitiveCheckbox = document.createElement('input'); sensitiveCheckbox.type = 'checkbox'; sensitiveCheckbox.checked = GM_getValue('show_sensitive', false); sensitiveCheckbox.style.marginRight = '10px'; sensitiveCheckbox.onchange = () => { GM_setValue('show_sensitive', sensitiveCheckbox.checked); show_sensitive = sensitiveCheckbox.checked; // 重新注入样式 document.head.insertAdjacentHTML('beforeend', ``); }; sensitiveLabel.innerText = lang.dialog.show_sensitive; sensitiveLabel.insertBefore(sensitiveCheckbox, sensitiveLabel.firstChild); sensitiveConfig.appendChild(sensitiveLabel); configArea.appendChild(sensitiveConfig); // 3. 启用ZIP打包 const zipConfig = document.createElement('div'); zipConfig.style.marginBottom = '15px'; zipConfig.style.padding = '10px'; zipConfig.style.border = '1px solid #eee'; zipConfig.style.borderRadius = '5px'; const zipLabel = document.createElement('label'); zipLabel.style.display = 'flex'; zipLabel.style.alignItems = 'center'; const zipCheckbox = document.createElement('input'); zipCheckbox.type = 'checkbox'; zipCheckbox.checked = GM_getValue('enable_packaging', true); zipCheckbox.style.marginRight = '10px'; zipCheckbox.onchange = () => GM_setValue('enable_packaging', zipCheckbox.checked); zipLabel.innerText = lang.enable_packaging; zipLabel.insertBefore(zipCheckbox, zipLabel.firstChild); zipConfig.appendChild(zipLabel); configArea.appendChild(zipConfig); // 4. 文件名格式 const filenameConfig = document.createElement('div'); filenameConfig.style.marginBottom = '15px'; filenameConfig.style.padding = '10px'; filenameConfig.style.border = '1px solid #eee'; filenameConfig.style.borderRadius = '5px'; const filenameTitle = document.createElement('div'); filenameTitle.innerText = lang.dialog.pattern; filenameTitle.style.marginBottom = '10px'; filenameTitle.style.fontWeight = 'bold'; const filenameInput = document.createElement('textarea'); filenameInput.value = GM_getValue('filename', filenameTemplate); filenameInput.style.width = '100%'; filenameInput.style.minHeight = '80px'; filenameInput.style.padding = '8px'; filenameInput.style.boxSizing = 'border-box'; filenameInput.style.border = '1px solid #ccc'; filenameInput.style.borderRadius = '5px'; filenameInput.onchange = () => GM_setValue('filename', filenameInput.value); // 变量提示 const varTips = document.createElement('div'); varTips.style.marginTop = '10px'; varTips.style.display = 'flex'; varTips.style.flexWrap = 'wrap'; varTips.style.gap = '8px'; const varList = [ { key: '{user-name}', tip: '用户名' }, { key: '{user-id}', tip: '@后的用户名' }, { key: '{status-id}', tip: '推文ID' }, { key: '{date-time}', tip: 'UTC发布时间' }, { key: '{date-time-local}', tip: '本地时间' }, { key: '{file-type}', tip: '媒体类型(photo/video)' }, { key: '{file-name}', tip: '原始文件名' } ]; varList.forEach(item => { const tag = document.createElement('span'); tag.className = 'tmd-tag'; tag.innerText = item.key; tag.title = item.tip; tag.onclick = () => { const start = filenameInput.selectionStart; const end = filenameInput.selectionEnd; filenameInput.value = filenameInput.value.substring(0, start) + item.key + filenameInput.value.substring(end); filenameInput.selectionStart = filenameInput.selectionEnd = start + item.key.length; }; varTips.appendChild(tag); }); filenameConfig.appendChild(filenameTitle); filenameConfig.appendChild(filenameInput); filenameConfig.appendChild(varTips); configArea.appendChild(filenameConfig); // 保存按钮 const saveBtn = document.createElement('div'); saveBtn.className = 'tmd-btn'; saveBtn.innerText = lang.dialog.save; saveBtn.style.marginLeft = 'auto'; saveBtn.style.display = 'block'; saveBtn.onclick = () => mask.remove(); configArea.appendChild(saveBtn); panel.appendChild(configArea); mask.appendChild(panel); document.body.appendChild(mask); } // -------------------------- 按钮初始化 -------------------------- // 普通下载按钮 const startBtn = document.createElement('button'); startBtn.innerText = '普通图片下载(ZIP)'; Object.assign(startBtn.style, { position: 'fixed', top: '20px', right: '20px', zIndex: 10000, padding: '12px 20px', backgroundColor: '#1DA1F2', color: '#fff', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '14px', marginBottom: '10px' }); startBtn.onclick = () => { clearTimeout(hideTimeoutId); startBtn.disabled = true; cancelBtn.style.display = 'block'; autoScrollAndDownloadImages(); }; // API下载按钮 const apiStartBtn = document.createElement('button'); apiStartBtn.innerText = 'API下载(ZIP)'; Object.assign(apiStartBtn.style, { position: 'fixed', top: '70px', right: '20px', zIndex: 10000, padding: '12px 20px', backgroundColor: '#1DA1F2', color: '#fff', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '14px', marginBottom: '10px' }); apiStartBtn.onclick = () => { clearTimeout(hideTimeoutId); apiStartBtn.disabled = true; cancelBtn.style.display = 'block'; autoScrollAndDownloadAPI(); }; // 取消按钮 const cancelBtn = document.createElement('button'); cancelBtn.innerText = '❌ 取消'; Object.assign(cancelBtn.style, { position: 'fixed', top: '120px', right: '20px', zIndex: 10000, padding: '12px 20px', backgroundColor: '#ff4d4f', color: '#fff', border: 'none', borderRadius: '5px', cursor: 'pointer', display: 'none', fontSize: '14px' }); cancelBtn.onclick = () => { cancelDownload = true; Downloader.cancel(); cancelBtn.innerText = '⏳ 停止中...'; // 启用原按钮 startBtn.disabled = false; apiStartBtn.disabled = false; }; // 设置按钮 const settingBtn = document.createElement('button'); settingBtn.innerText = '⚙️ 设置'; Object.assign(settingBtn.style, { position: 'fixed', top: '170px', right: '20px', zIndex: 10000, padding: '12px 20px', backgroundColor: '#666', color: '#fff', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '14px' }); settingBtn.onclick = openSettings; // 添加按钮到页面 document.body.appendChild(startBtn); //document.body.appendChild(apiStartBtn); document.body.appendChild(cancelBtn); //document.body.appendChild(settingBtn); // -------------------------- 初始化 -------------------------- (async () => { await initBaseConfig(); updateProgress('准备就绪:点击按钮开始下载(支持ZIP打包)'); // 3秒后自动隐藏初始提示 hideTimeoutId = setTimeout(() => { progressBox.style.display = 'none'; }, 3000); })(); })();