// ==UserScript==
// @name X 博主综合操作脚本(ZIP打包版)
// @namespace http://tampermonkey.net/
// @version 2.0
// @description 网页扫描下载(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);
})();
})();