Greasy Fork is available in English.
[UI重构] 悬浮球控制 | 独立接口监听开关 | 暗黑模式 | 自动解析 X/Twitter 高清原图/视频 | 自动多级目录 | 发送至 Motrix | 🚫 彻底去除敏感内容遮罩
// ==UserScript==
// @name X/Twitter 年龄限制解除
// @namespace http://tampermonkey.net/
// @version 2.4.0
// @description [UI重构] 悬浮球控制 | 独立接口监听开关 | 暗黑模式 | 自动解析 X/Twitter 高清原图/视频 | 自动多级目录 | 发送至 Motrix | 🚫 彻底去除敏感内容遮罩
// @author Gemini & JHC_Style
// @match https://x.com/*
// @match https://twitter.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @connect localhost
// @connect 127.0.0.1
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ================= 状态与配置管理 =================
const STATE_COLORS = {
IDLE: 'rgb(108, 92, 231)', // 空闲/就绪 (紫色)
SUCCESS: 'rgb(46, 204, 113)', // 成功 (绿色)
ERROR: 'rgb(231, 76, 60)', // 错误 (红色)
PROCESSING: 'rgb(241, 196, 15)' // 处理中 (黄色)
};
const DEFAULTS = {
enabled: true,
rpcUrl: 'http://localhost:16800/jsonrpc',
rpcToken: '',
debugMode: false,
darkMode: false, // 暗黑模式
listenUserTweets: true, // 监听用户主页/时间线
listenTweetDetail: true, // 监听推文详情
listenSearch: true, // 监听搜索结果
unlockSensitive: true // 🟢 新增:解除敏感内容限制
};
function getSettings() {
return {
enabled: GM_getValue('x_enabled', DEFAULTS.enabled),
rpcUrl: GM_getValue('x_rpcUrl', DEFAULTS.rpcUrl),
rpcToken: GM_getValue('x_rpcToken', DEFAULTS.rpcToken),
debugMode: GM_getValue('x_debugMode', DEFAULTS.debugMode),
darkMode: GM_getValue('x_darkMode', DEFAULTS.darkMode),
listenUserTweets: GM_getValue('x_listenUserTweets', DEFAULTS.listenUserTweets),
listenTweetDetail: GM_getValue('x_listenTweetDetail', DEFAULTS.listenTweetDetail),
listenSearch: GM_getValue('x_listenSearch', DEFAULTS.listenSearch),
unlockSensitive: GM_getValue('x_unlockSensitive', DEFAULTS.unlockSensitive)
};
}
let isUiReady = false;
// ================= 工具函数 =================
function log(msg, data) {
const s = getSettings();
if (s.debugMode) {
console.log(`%c[X-Motrix] ${msg}`, 'color: #0984e3; font-weight: bold;', data || '');
}
}
function prettyLog(title, sub, msg, color) {
if (isUiReady) {
showToast(title, sub, msg, color);
} else {
console.log(`[${title}] ${sub}: ${msg}`);
}
}
function showToast(t, s, m, c) {
const container = document.getElementById('x-toast-container');
if (container) {
const d = document.createElement('div');
d.className = 'x-toast-msg';
// 实时同步暗黑模式:优先读取 UI 复选框的实时状态,如果 UI 未加载则读取存储配置
const uiCheckbox = document.getElementById('x-ui-dark-mode');
const isDark = uiCheckbox ? uiCheckbox.checked : getSettings().darkMode;
if (isDark) {
d.classList.add('x-dark-mode');
}
d.style.borderColor = c;
d.innerHTML = `<b>${t} ${s}</b><br><span class="x-toast-desc">${m}</span>`;
container.appendChild(d);
setTimeout(() => {
d.style.opacity = '0';
d.style.transform = 'translateX(100%)';
setTimeout(() => d.remove(), 300);
}, 3000);
}
}
function updateBallColor(stateKey) {
const btn = document.getElementById('x-helper-btn');
if (btn && STATE_COLORS[stateKey]) {
btn.style.background = STATE_COLORS[stateKey];
// 1.5秒后恢复为空闲色
if (stateKey !== 'IDLE') {
setTimeout(() => {
btn.style.background = STATE_COLORS.IDLE;
}, 1500);
}
}
}
/**
* 格式化当前时间 YYYYMMDD_HHmmss
*/
function getFormattedTime() {
const now = new Date();
return now.getFullYear().toString() +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') + "_" +
now.getHours().toString().padStart(2, '0') +
now.getMinutes().toString().padStart(2, '0') +
now.getSeconds().toString().padStart(2, '0');
}
/**
* 文件名非法字符清理
*/
function sanitizeFileName(str) {
return (str || "").replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, " ").trim();
}
/**
* 获取高清原图链接
*/
function getHighResUrl(url) {
if (!url) return null;
if (url.includes('name=orig')) return url;
const match = url.match(/^(https?:\/\/.*)\.([a-zA-Z0-9]+)(\?.*)?$/);
if (match) {
return `${match[1]}?format=${match[2]}&name=orig`;
}
return url;
}
// ================= 核心逻辑:推送到 Motrix =================
function sendToMotrix(mediaUrl, context, index, type) {
const s = getSettings();
if (!s.enabled) return;
if (!mediaUrl) return;
// 1. 构造路径: 域名/作者名/描述_时间.后缀
const domain = location.hostname;
const author = sanitizeFileName(context.userName || "Unknown_User");
// 截取描述前50个字符作为文件名一部分
let desc = sanitizeFileName(context.text || context.tweetId);
if (desc.length > 50) desc = desc.substring(0, 50);
const timeStr = getFormattedTime();
// 提取扩展名
let ext = 'jpg';
if (type === 'video') {
ext = 'mp4';
} else {
const extMatch = mediaUrl.match(/[?&]format=([a-zA-Z0-9]+)/);
if (extMatch) {
ext = extMatch[1];
} else {
const dotMatch = mediaUrl.match(/\.([a-zA-Z0-9]+)$/);
if (dotMatch) ext = dotMatch[1];
}
}
const filename = `${domain}/${author}/${desc}_${timeStr}_p${index}.${ext}`;
// 2. 构造 RPC 请求
const payload = {
jsonrpc: '2.0',
id: 'X-Motrix-' + Date.now(),
method: 'aria2.addUri',
params: [
[mediaUrl],
{
"out": filename,
"header": [
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Referer: https://x.com/",
"Origin: https://x.com"
],
"check-certificate": "false",
"connect-timeout": "10",
"timeout": "20",
"max-tries": "0",
"retry-wait": "3",
"split": "8", // 连接数建议 4-8,不要过大
"min-split-size": "1M"
}
]
};
if (s.rpcToken) {
payload.params.unshift(`token:${s.rpcToken}`);
}
updateBallColor('PROCESSING');
GM_xmlhttpRequest({
method: 'POST',
url: s.rpcUrl,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(payload),
onload: function(response) {
if (response.status === 200) {
log(`✅ 推送成功: ${filename}`);
prettyLog("✅", "推送成功", `文件: ${filename.split('/').pop()}`, STATE_COLORS.SUCCESS);
updateBallColor('SUCCESS');
} else {
log(`❌ 推送失败: ${response.responseText}`);
prettyLog("❌", "推送失败", "RPC 返回错误,请检查配置", STATE_COLORS.ERROR);
updateBallColor('ERROR');
}
},
onerror: function(err) {
log(`❌ RPC 连接错误:`, err);
prettyLog("❌", "连接错误", "无法连接 Motrix,请检查运行状态", STATE_COLORS.ERROR);
updateBallColor('ERROR');
}
});
}
// ================= 解析逻辑 =================
function extractMediaFromTweet(tweet) {
if (!tweet || !tweet.legacy) return 0;
const legacy = tweet.legacy;
const extendedEntities = legacy.extended_entities;
if (!extendedEntities || !extendedEntities.media || !Array.isArray(extendedEntities.media)) return 0;
// 提取推文上下文信息
const userName = tweet.core?.user_results?.result?.core?.name || legacy.user_id_str || 'unknown';
const fullText = legacy.full_text || "";
const tweetId = legacy.rest_id;
const context = {
userName: userName,
text: fullText,
tweetId: tweetId
};
let extractedCount = 0;
extendedEntities.media.forEach((media, index) => {
// 图片
if (media.type === 'photo' && media.media_url_https) {
const highResUrl = getHighResUrl(media.media_url_https);
sendToMotrix(highResUrl, context, index, 'photo');
extractedCount++;
log(`📷 发现图片: ${highResUrl}`);
}
// 视频
else if (media.type === 'video' && media.video_info) {
const variants = media.video_info.variants;
if (variants) {
const mp4Variants = variants.filter(v => v.content_type === 'video/mp4');
// 按码率排序取最大
mp4Variants.sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0));
if (mp4Variants.length > 0) {
const videoUrl = mp4Variants[0].url;
sendToMotrix(videoUrl, context, index, 'video');
extractedCount++;
log(`🎥 发现视频: ${videoUrl}`);
}
}
}
});
return extractedCount;
}
function handleApiResponse(jsonStr) {
const s = getSettings();
if (!s.enabled) return;
try {
const data = JSON.parse(jsonStr);
let instructions = null;
let totalExtracted = 0;
// 路径匹配与独立开关判断
// 1. UserTweets (主页/时间线)
if (data?.data?.user?.result?.timeline?.timeline?.instructions) {
if (!s.listenUserTweets) {
log('🚫 UserTweets 接口被禁用,跳过解析');
return;
}
instructions = data.data.user.result.timeline.timeline.instructions;
log('检测到 UserTweets 数据结构');
}
// 2. TweetDetail (详情页)
else if (data?.data?.threaded_conversation_with_injections_v2?.instructions) {
if (!s.listenTweetDetail) {
log('🚫 TweetDetail 接口被禁用,跳过解析');
return;
}
instructions = data.data.threaded_conversation_with_injections_v2.instructions;
log('检测到 TweetDetail 数据结构');
}
// 3. Search (搜索结果)
else if (data?.data?.search_by_raw_query?.search_timeline?.timeline?.instructions) {
if (!s.listenSearch) {
log('🚫 Search 接口被禁用,跳过解析');
return;
}
instructions = data.data.search_by_raw_query.search_timeline.timeline.instructions;
log('检测到 Search 数据结构');
}
if (!instructions || !Array.isArray(instructions)) return;
const processTweetResult = (result) => {
if (!result) return 0;
const tweet = result.tweet || result;
// 排除广告
if (tweet.legacy && tweet.legacy.extended_entities) {
return extractMediaFromTweet(tweet);
}
return 0;
};
instructions.forEach(instruction => {
if (instruction.type === 'TimelineAddEntries' && Array.isArray(instruction.entries)) {
instruction.entries.forEach(entry => {
// 标准推文
const tweetResult = entry?.content?.itemContent?.tweet_results?.result;
totalExtracted += processTweetResult(tweetResult);
// 评论/对话
const items = entry?.content?.items;
if (items && Array.isArray(items)) {
items.forEach(item => {
const itemTweetResult = item?.item?.itemContent?.tweet_results?.result;
totalExtracted += processTweetResult(itemTweetResult);
});
}
});
} else if (instruction.type === 'TimelineAddToModule' && Array.isArray(instruction.moduleItems)) {
instruction.moduleItems.forEach(item => {
const tweetResult = item?.item?.itemContent?.tweet_results?.result;
totalExtracted += processTweetResult(tweetResult);
});
}
});
if (totalExtracted > 0) {
log(`📊 本批次共解析 ${totalExtracted} 个媒体文件`);
}
} catch (e) {
console.error('[X-Motrix] JSON 解析异常:', e);
}
}
// ================= Hook XHR (核心修改) =================
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
// 获取原始的 responseText 描述符,用于后续在 getter 中调用
const originalResponseTextDesc = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText');
XMLHttpRequest.prototype.open = function(method, url) {
this._url = url;
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
// 1. 监听加载完成 (被动解析用于下载)
this.addEventListener('load', function() {
if (this._url && (
this._url.includes('UserTweets') ||
this._url.includes('TweetDetail') ||
this._url.includes('SearchTimeline')
) && this.responseText) {
handleApiResponse(this.responseText);
}
});
// 2. 劫持响应数据 (主动修改用于前端渲染) - 🟢 新增功能
// 只有涉及推文数据的接口才需要处理
if (this._url && (
this._url.includes('UserTweets') ||
this._url.includes('TweetDetail') ||
this._url.includes('SearchTimeline')
)) {
// 定义 getter 覆盖原有属性
Object.defineProperty(this, 'responseText', {
get: function() {
// 调用原始 getter 获取真实数据
let rawText = originalResponseTextDesc.get.call(this);
const s = getSettings();
// 如果开启了解锁敏感内容,且数据是字符串,则进行替换
if (s.unlockSensitive && typeof rawText === 'string') {
try {
// 暴力替换:
// 1. possibly_sensitive: true -> false (推文级开关)
// 2. profile_interstitial_type: "sensitive_media" -> "" (主页级警告)
// 3. mediaVisibilityResults -> mediaVisibilityResults_bypass (直接破坏遮罩层数据结构,防止前端读取)
return rawText.replace(/"possibly_sensitive":\s*true/g, '"possibly_sensitive":false')
.replace(/"profile_interstitial_type":\s*"sensitive_media"/g, '"profile_interstitial_type":""')
.replace(/"mediaVisibilityResults":/g, '"mediaVisibilityResults_bypass":');
} catch (e) {
console.error('[X-Motrix] 敏感内容解锁失败:', e);
return rawText;
}
}
return rawText;
},
configurable: true
});
}
return originalSend.apply(this, arguments);
};
// ================= UI 构建 =================
function createUI() {
// 复刻抖音脚本样式,增加暗黑模式适配 CSS
GM_addStyle(`
#x-helper-btn { position: fixed; z-index: 2147483647; width: 50px; height: 50px; background: ${STATE_COLORS.IDLE}; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: grab; box-shadow: 0 8px 25px rgba(108, 92, 231, 0.4); font-weight: bold; font-family: sans-serif; transition: transform 0.1s, box-shadow 0.2s; border: 2px solid rgba(255,255,255,0.2); user-select: none; }
#x-helper-btn:active { cursor: grabbing; transform: scale(0.95); }
#x-helper-btn:hover { box-shadow: 0 10px 30px rgba(108, 92, 231, 0.6); }
#x-settings-panel { position: fixed; z-index: 2147483647; width: 320px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); padding: 20px; display: none; border-radius: 16px; box-shadow: 0 20px 50px rgba(0,0,0,0.15); border: 1px solid rgba(255,255,255,0.6); font-family: 'Segoe UI', Roboto, sans-serif; color: #2d3436; flex-direction: column; transition: opacity 0.2s; }
#x-settings-panel h3 { margin: 0 0 15px 0; font-size: 18px; color: #2d3436; border-bottom: 1px solid #eee; padding-bottom: 10px; }
/* 暗黑模式适配 - 面板 */
#x-settings-panel.x-dark-mode { background: rgba(45, 52, 54, 0.95); color: #dfe6e9; border: 1px solid rgba(255,255,255,0.1); }
#x-settings-panel.x-dark-mode h3 { color: #dfe6e9; border-bottom-color: rgba(255,255,255,0.1); }
#x-settings-panel.x-dark-mode .x-label { color: #dfe6e9; }
#x-settings-panel.x-dark-mode .x-toggle-label { color: #b2bec3; }
#x-settings-panel.x-dark-mode .x-input { background: #1e272e; color: #ecf0f1; border: 1px solid #636e72; }
#x-settings-panel.x-dark-mode .x-input:focus { border-color: #a29bfe; }
#x-settings-panel.x-dark-mode .x-btn-secondary { background: #636e72; color: #dfe6e9; }
#x-settings-panel.x-dark-mode .x-slider { background-color: #636e72; }
#x-settings-panel.x-dark-mode .x-info-box { background: #2d3436; color: #b2bec3; border: 1px solid #636e72; }
#x-settings-panel.x-dark-mode .x-section-box { background: rgba(255,255,255,0.05); border: 1px dashed rgba(255,255,255,0.2); }
.x-group { margin-bottom: 15px; }
.x-label { font-size: 12px; color: #636e72; margin-bottom: 6px; font-weight: 700; display: block; }
.x-input { all: initial; display: block; width: 100%; box-sizing: border-box; padding: 10px 12px; border: 1px solid #b2bec3; border-radius: 8px; background: #fff; color: #2d3436; outline: none; transition: border 0.2s; font-size: 13px !important; font-family: sans-serif !important; margin-top: 5px; }
.x-input:focus { border-color: #6c5ce7; box-shadow: 0 0 0 3px rgba(108, 92, 231, 0.1); }
.x-toggle-wrapper { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.x-toggle-label { font-size: 14px; color: #2d3436; font-weight: 500; }
.x-toggle { position: relative; display: inline-block; width: 44px; height: 24px; }
.x-toggle input { opacity: 0; width: 0; height: 0; }
.x-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #b2bec3; transition: .4s; border-radius: 34px; }
.x-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
input:checked + .x-slider { background-color: #00b894 !important; }
input:checked + .x-slider:before { transform: translateX(20px); }
.x-actions { display: flex; gap: 10px; margin-top: 15px; padding-top: 10px; border-top: 1px solid rgba(0,0,0,0.05); }
.x-btn { flex: 1; padding: 10px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; transition: opacity 0.2s; }
.x-btn-primary { background: #6c5ce7; color: white; box-shadow: 0 4px 15px rgba(108, 92, 231, 0.3); }
.x-btn-secondary { background: #dfe6e9; color: #636e72; }
.x-btn:hover { opacity: 0.9; }
.x-info-box { background:#f1f2f6; padding:10px; border-radius:8px; font-size:11px; color:#636e72; }
.x-section-box { border:1px dashed #6c5ce7; padding:10px; border-radius:8px; margin-bottom:15px; background:rgba(108, 92, 231, 0.05); }
#x-toast-container { position: fixed; top: 80px; right: 20px; z-index: 2147483600; pointer-events: none; }
.x-toast-msg { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(12px); margin-bottom: 10px; padding: 8px 12px; border-left: 4px solid; border-radius: 6px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); width: 220px; pointer-events: auto; animation: slideIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); border: 1px solid rgba(255,255,255,0.4); color: #2d3436; font-family: sans-serif; font-size: 12px; line-height: 1.4; }
/* 暗黑模式适配 - Toast */
.x-toast-msg.x-dark-mode { background: rgba(40, 40, 40, 0.95); color: #dfe6e9; border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 5px 20px rgba(0,0,0,0.3); }
.x-toast-msg.x-dark-mode .x-toast-desc { color: #b2bec3 !important; }
.x-toast-desc { font-size:12px; color:#636e72; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
`);
// 等待页面加载
const initInterval = setInterval(() => {
if (document.body) {
clearInterval(initInterval);
renderElements();
}
}, 100);
}
function renderElements() {
if (document.getElementById('x-helper-btn')) return;
// 1. 悬浮球 (替换为 X Logo SVG)
const btn = document.createElement('div');
btn.id = 'x-helper-btn';
// 使用 X (Twitter) 官方 SVG 路径
btn.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true" style="width: 24px; height: 24px; fill: white;"><g><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></g></svg>`;
btn.title = "X-Motrix 下载助手";
const savedTop = GM_getValue('x_btn_top', '20%');
const savedLeft = GM_getValue('x_btn_left', 'calc(100% - 70px)');
btn.style.top = savedTop;
btn.style.left = savedLeft;
// 2. 拖拽逻辑
let isDragging = false;
let dragStartTime = 0;
let dragRaf = null;
btn.onmousedown = function (event) {
isDragging = false;
dragStartTime = Date.now();
let shiftX = event.clientX - btn.getBoundingClientRect().left;
let shiftY = event.clientY - btn.getBoundingClientRect().top;
function moveAt(pageX, pageY) {
isDragging = true;
if (dragRaf) cancelAnimationFrame(dragRaf);
dragRaf = requestAnimationFrame(() => {
btn.style.left = pageX - shiftX + 'px';
btn.style.top = pageY - shiftY + 'px';
const panel = document.getElementById('x-settings-panel');
if (panel && panel.style.display !== 'none') {
repositionPanel(btn, panel);
}
});
}
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
document.addEventListener('mousemove', onMouseMove);
document.onmouseup = function () {
document.removeEventListener('mousemove', onMouseMove);
document.onmouseup = null;
if (dragRaf) cancelAnimationFrame(dragRaf);
if (isDragging) {
GM_setValue('x_btn_top', btn.style.top);
GM_setValue('x_btn_left', btn.style.left);
}
};
};
btn.ondragstart = () => false;
// 点击打开/关闭面板
btn.addEventListener('click', (e) => {
e.stopPropagation();
if (isDragging || (Date.now() - dragStartTime > 200)) return;
const p = document.getElementById('x-settings-panel');
if (p) {
if (p.style.display === 'none') {
p.style.display = 'flex';
syncUI(); // 打开时同步数据
repositionPanel(btn, p);
} else {
p.style.display = 'none';
}
}
});
document.body.appendChild(btn);
// 3. 配置面板
const panel = document.createElement('div');
panel.id = 'x-settings-panel';
// 初始应用暗黑模式
if (getSettings().darkMode) panel.classList.add('x-dark-mode');
panel.innerHTML = `
<h3>X/Twitter Motrix 助手</h3>
<div class="x-toggle-wrapper">
<span class="x-toggle-label">🟢 启用总开关</span>
<label class="x-toggle"><input type="checkbox" id="x-ui-enable"><span class="x-slider"></span></label>
</div>
<div class="x-toggle-wrapper" style="margin-bottom:15px; background:rgba(231, 76, 60, 0.1); padding:8px; border-radius:6px; border:1px dashed rgba(231, 76, 60, 0.4);">
<span class="x-toggle-label" style="color:#e74c3c;">🔞 解除敏感内容限制</span>
<label class="x-toggle"><input type="checkbox" id="x-ui-unlock-sensitive"><span class="x-slider"></span></label>
</div>
<div class="x-section-box">
<div class="x-label" style="color:#6c5ce7; margin-bottom:8px;">📡 接口监听设置</div>
<div class="x-toggle-wrapper">
<span class="x-toggle-label" style="font-size:12px">👤 主页/时间线 (UserTweets)</span>
<label class="x-toggle" style="transform:scale(0.8);"><input type="checkbox" id="x-ui-listen-user"><span class="x-slider"></span></label>
</div>
<div class="x-toggle-wrapper">
<span class="x-toggle-label" style="font-size:12px">📄 推文详情页 (TweetDetail)</span>
<label class="x-toggle" style="transform:scale(0.8);"><input type="checkbox" id="x-ui-listen-detail"><span class="x-slider"></span></label>
</div>
<div class="x-toggle-wrapper">
<span class="x-toggle-label" style="font-size:12px">🔍 搜索结果页 (Search)</span>
<label class="x-toggle" style="transform:scale(0.8);"><input type="checkbox" id="x-ui-listen-search"><span class="x-slider"></span></label>
</div>
</div>
<div class="x-group">
<span class="x-label">📡 Motrix RPC 地址</span>
<input class="x-input" id="x-ui-rpc" placeholder="http://127.0.0.1:16800/jsonrpc">
</div>
<div class="x-group">
<span class="x-label">🔑 RPC 密钥 (Token)</span>
<input class="x-input" id="x-ui-token" type="password" placeholder="留空则无密钥">
</div>
<div class="x-toggle-wrapper">
<span class="x-toggle-label">🌙 暗黑模式</span>
<label class="x-toggle"><input type="checkbox" id="x-ui-dark-mode"><span class="x-slider"></span></label>
</div>
<div class="x-toggle-wrapper">
<span class="x-toggle-label">🐞 调试日志 (Console)</span>
<label class="x-toggle"><input type="checkbox" id="x-ui-debug"><span class="x-slider"></span></label>
</div>
<div class="x-actions">
<button class="x-btn x-btn-secondary" id="x-ui-close">关闭</button>
<button class="x-btn x-btn-primary" id="x-ui-save">💾 保存配置</button>
</div>
`;
// 防止点击面板触发页面其他事件
panel.addEventListener('click', (e) => e.stopPropagation());
// 防止输入框触发页面快捷键
panel.querySelectorAll('input').forEach(el => {
el.addEventListener('keydown', (e) => e.stopPropagation());
});
document.body.appendChild(panel);
// Toast 容器
const toastContainer = document.createElement('div');
toastContainer.id = 'x-toast-container';
document.body.appendChild(toastContainer);
// 绑定事件
document.getElementById('x-ui-close').onclick = () => {
document.getElementById('x-settings-panel').style.display = 'none';
};
document.getElementById('x-ui-save').onclick = saveUI;
// 绑定暗黑模式即时预览
document.getElementById('x-ui-dark-mode').addEventListener('change', (e) => {
const p = document.getElementById('x-settings-panel');
if (e.target.checked) p.classList.add('x-dark-mode'); else p.classList.remove('x-dark-mode');
});
isUiReady = true;
log('UI 初始化完成');
}
// 智能定位面板
function repositionPanel(btn, panel) {
const btnRect = btn.getBoundingClientRect();
const panelWidth = 320;
const margin = 15;
// 默认显示在左侧
let left = btnRect.left - panelWidth - margin;
let top = btnRect.top;
// 如果左边空间不足,显示在右边
if (left < 10) {
left = btnRect.right + margin;
}
// 垂直防溢出
if (top + panel.offsetHeight > window.innerHeight) {
top = window.innerHeight - panel.offsetHeight - 10;
}
if (top < 10) top = 10;
panel.style.top = top + 'px';
panel.style.left = left + 'px';
}
// 从存储同步到 UI
function syncUI() {
const s = getSettings();
document.getElementById('x-ui-enable').checked = s.enabled;
document.getElementById('x-ui-listen-user').checked = s.listenUserTweets;
document.getElementById('x-ui-listen-detail').checked = s.listenTweetDetail;
document.getElementById('x-ui-listen-search').checked = s.listenSearch;
document.getElementById('x-ui-rpc').value = s.rpcUrl;
document.getElementById('x-ui-token').value = s.rpcToken;
document.getElementById('x-ui-dark-mode').checked = s.darkMode;
document.getElementById('x-ui-debug').checked = s.debugMode;
document.getElementById('x-ui-unlock-sensitive').checked = s.unlockSensitive;
// 同步面板暗黑样式
const panel = document.getElementById('x-settings-panel');
if (s.darkMode) panel.classList.add('x-dark-mode'); else panel.classList.remove('x-dark-mode');
}
// 从 UI 保存到存储
function saveUI() {
const enabled = document.getElementById('x-ui-enable').checked;
const listenUser = document.getElementById('x-ui-listen-user').checked;
const listenDetail = document.getElementById('x-ui-listen-detail').checked;
const listenSearch = document.getElementById('x-ui-listen-search').checked;
const rpcUrl = document.getElementById('x-ui-rpc').value;
const rpcToken = document.getElementById('x-ui-token').value;
const darkMode = document.getElementById('x-ui-dark-mode').checked;
const debugMode = document.getElementById('x-ui-debug').checked;
const unlockSensitive = document.getElementById('x-ui-unlock-sensitive').checked;
GM_setValue('x_enabled', enabled);
GM_setValue('x_listenUserTweets', listenUser);
GM_setValue('x_listenTweetDetail', listenDetail);
GM_setValue('x_listenSearch', listenSearch);
GM_setValue('x_rpcUrl', rpcUrl);
GM_setValue('x_rpcToken', rpcToken);
GM_setValue('x_darkMode', darkMode);
GM_setValue('x_debugMode', debugMode);
GM_setValue('x_unlockSensitive', unlockSensitive);
document.getElementById('x-settings-panel').style.display = 'none';
prettyLog("💾", "配置已保存", "新的设置即刻生效 (敏感内容需刷新页面)", STATE_COLORS.SUCCESS);
}
// 启动
createUI();
})();