// ==UserScript==
// @name X岛-EX
// @namespace https://github.com/SayaGoodBye/nmbxd-EX
// @version 3.12.0
// @description X岛-EX 网页端增强,移动端般的浏览体验:快捷切换饼干-发送前二次确认 / 添加页首页码 / 关闭图片水印 / 预览真实饼干 / 隐藏无标题-无名氏-版规 / 显示外部图床 / 自动刷新饼干 toast提示 / 无缝翻页-自动翻页 / 默认原图+控件 / 新标签打开串 / 优化引用弹窗 / 拓展引用格式 / 当页回复编号 / 扩展坞增强 / 拦截回复中间页 / 颜文字拓展 / 高亮PO主 / 发串UI调整 / 『分组标记饼干』 / 『屏蔽饼干』 / 『只看饼干』 / 『屏蔽关键词』- 隐藏-折叠 / 增强X岛匿名版 / 板块页快速回复 / 展开板块页长串 / 野生搜索酱 / unvcode-零宽空格模式 / 侧边栏收起 / 图片显示模式 / 图片自动压缩-非法图像格式(无GCT)GIF重编码 / 链接自动识别 / 使用数据-设置项-导入导出-剪贴板文件 / 常用串 / 浏览历史 / 发言历史 / 移动端订阅 / 阅图模式 。
// @author XY
// @match https://*.nmbxd1.com/*
// @match https://*.nmbxd.com/*
// @match https://nmb-search.166666666.xyz/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addValueChangeListener
// @grant GM_xmlhttpRequest
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addStyle
// @grant unsafeWindow
// @connect nmbxd1.com
// @connect www.nmbxd1.com
// @connect nmbxd.com
// @connect www.nmbxd.com
// @connect nmb-search.166666666.xyz
// @connect image.nmb.best
// @connect api.nmb.best
// @connect raw.githubusercontent.com
// @connect cdn.jsdelivr.net
// @connect fastly.jsdelivr.net
// @connect update.greasyfork.org
// @connect scriptcat.org
// @connect code.jquery.com
// @connect unpkg.com
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @require https://cdn.jsdelivr.net/npm/apng-js@1.1.5/lib/index.js
// @require https://unpkg.com/upng-js@2.1.0/UPNG.js
// @icon https://image.nmb.best/image/2026-06-03/6a1fcea41fad3.png
// @icon64 https://image.nmb.best/image/2026-06-03/6a1fced8e0e64.png
// @license WTFPL
// @changelog 新增:\n1.新增WebP、HEIF等格式自动转化为PNG以及压缩链路。请注意HEIF图片无法预览。\n2.添加基于获取版块页首页信息拉起发串信息的回退机制,用于“测试”版块发串信息的获取;同时,“测试”版块默认发串模式。\n\n优化:\n1.优化“浏览历史/发言历史/我的订阅”以及拓展坞启动的速度。\n2.优化了图片懒加载并发队列与动态预加载机制,现在可以更快加载滚动方向上的图像原图。\n3.阅图模式按钮与图片显示模式状态同步。\n\n修复:\n1.修复了部分图片错误进入压缩循环且无法提交的问题。\n2.修复在发送前二次确认窗口中使用鼠标点击的饼干没有被选择并作用,而实际使用列表最后一个饼干发送的问题。\n3.修复拓展引用弹窗在打开多层后,较早的层级无法拖拽移动的问题。\n
// @note 特别感谢:icon由9HrD12x设计并绘制 >>No.68765505
// @note 致谢:切饼代码移植自[XD-Enhance](https://greasyfork.org/zh-CN/scripts/438164-xd-enhance)
// @note 致谢:外部图床代码二改自[显示x岛图片链接指向的图片](https://greasyfork.org/zh-CN/scripts/546024-%E6%98%BE%E7%A4%BAx%E5%B2%9B%E5%9B%BE%E7%89%87%E9%93%BE%E6%8E%A5%E6%8C%87%E5%90%91%E7%9A%84%E5%9B%BE%E7%89%87)
// @note 致谢:完整移植[增强x岛匿名版](https://greasyfork.org/zh-CN/scripts/513156-%E5%A2%9E%E5%BC%BAx%E5%B2%9B%E5%8C%BF%E5%90%8D%E7%89%88)
// @note 致谢:部分功能移植自[X岛-揭示板的增强型体验](https://greasyfork.org/zh-CN/scripts/497875-x%E5%B2%9B-%E6%8F%AD%E7%A4%BA%E6%9D%BF%E7%9A%84%E5%A2%9E%E5%BC%BA%E5%9E%8B%E4%BD%93%E9%AA%8C#%E8%BF%9E%E6%8E%A5%E7%9B%B4%E6%8E%A5%E8%B7%B3%E8%BD%AC)
// @note 致谢:来自4sYbzEX的搜索服务[野生搜索酱](https://www.nmbxd.com/t/64792841)
// @note 致谢:来自acVMxuv的[侧边栏优化](https://greasyfork.org/zh-CN/scripts/553143-x%E5%B2%9B%E4%BC%98%E5%8C%96%E5%B2%9B-%E4%BE%A7%E8%BE%B9%E6%A0%8F%E4%BC%98%E5%8C%96%E7%89%88)
// @run-at document-start
// ==/UserScript==
/* global $, jQuery */
// 更新渠道
// @downloadURL https://update.greasyfork.org/scripts/531005/X%E5%B2%9B-EX.user.js
// @updateURL https://update.greasyfork.org/scripts/531005/X%E5%B2%9B-EX.meta.js
// @downloadURL https://scriptcat.org/scripts/code/6289/X%E5%B2%9B-EX.user.js
// @updateURL https://scriptcat.org/scripts/code/6289/X%E5%B2%9B-EX.meta.js
(function($){
'use strict';
/* --------------------------------------------------
* tag 0. 通用与工具函数
* -------------------------------------------------- */
const VERSION = GM_info.script.version;
const XDEX_SINGLETON_OWNER_DATASET_KEY = 'xdexSingletonOwner';
const XDEX_SINGLETON_WAIT_MS = 100;
function getXDexRuntimeInfo(){
const declared = typeof globalThis !== 'undefined' ? globalThis.__xdexRuntime : null;
if (declared && declared.kind === 'extension') {
return Object.assign({ kind: 'extension' }, declared);
}
const scriptHandler = GM_info && (GM_info.scriptHandler || (GM_info.script && GM_info.script.handler)) || '';
return {
kind: 'userscript',
scriptHandler: scriptHandler || 'unknown'
};
}
function shouldExitForXDexSingleton(runtimeInfo){
const root = document.documentElement;
const owner = root && root.dataset ? root.dataset[XDEX_SINGLETON_OWNER_DATASET_KEY] : '';
return owner === 'extension' && (!runtimeInfo || runtimeInfo.kind !== 'extension');
}
function cat_version(){
console.log('[version]:', VERSION);
}
const XDEX_RUNTIME = getXDexRuntimeInfo();
function getXDexGmStorageReady(){
const ready = typeof globalThis !== 'undefined' ? globalThis.__xdexGmStorageReady : null;
return ready && typeof ready.then === 'function' ? ready : Promise.resolve();
}
const XDEX_GM_STORAGE_READY = getXDexGmStorageReady();
function scheduleXDexStartup(){
if (shouldExitForXDexSingleton(XDEX_RUNTIME)) return;
const startAfterStorageReady = () => {
if (shouldExitForXDexSingleton(XDEX_RUNTIME)) return;
startXDexRuntime();
};
if (XDEX_RUNTIME.kind !== 'extension') {
setTimeout(() => {
if (shouldExitForXDexSingleton(XDEX_RUNTIME)) return;
XDEX_GM_STORAGE_READY.then(() => {
startAfterStorageReady();
}, () => {
startAfterStorageReady();
});
}, XDEX_SINGLETON_WAIT_MS);
return;
}
XDEX_GM_STORAGE_READY.then(() => {
startAfterStorageReady();
}, () => {
startAfterStorageReady();
});
}
function startXDexRuntime(){
cat_version();
console.log('[runtime]:', XDEX_RUNTIME.kind, XDEX_RUNTIME);
function gmRequest(url, responseType = 'text', headers = null) {
return new Promise((resolve, reject) => {
const request = {
method: 'GET',
url,
responseType,
onload: (resp) => {
if (resp.status >= 200 && resp.status < 300) {
resolve(resp);
} else {
reject(new Error(`HTTP ${resp.status} ${url}`));
}
},
onerror: () => reject(new Error(`Request failed: ${url}`)),
ontimeout: () => reject(new Error(`Request timeout: ${url}`))
};
if (headers) request.headers = headers;
GM_xmlhttpRequest(request);
});
}
const toastQueue = [];
let isShowing = false;
function toast(msg, duration = 1800, options = {}) {
if (options.queue === false) {
showImmediateToast(msg, duration, options.key);
return;
}
toastQueue.push({ msg, duration });
if (!isShowing) showNextToast();
}
function showNextToast() {
if (toastQueue.length === 0) {
isShowing = false;
return;
}
isShowing = true;
const { msg, duration } = toastQueue.shift();
console.log('[toast]', msg);
// ✅ 每次创建一个新的 toast 节点
const $t = $(`
${msg}
`);
$('body').append($t);
$t.fadeIn(240).delay(duration).fadeOut(240, () => {
$t.remove(); // ✅ 动画结束后删除节点
showNextToast(); // ✅ 显示下一个
});
}
function showImmediateToast(msg, duration = 900, key = 'default') {
const safeKey = String(key || 'default').replace(/[^a-z0-9_-]/gi, '-');
let $t = $(`#xdex-immediate-toast-${safeKey}`);
if (!$t.length) {
$t = $(``);
$('body').append($t);
}
const el = $t[0];
const seq = (Number(el.__xdexImmediateToastSeq) || 0) + 1;
el.__xdexImmediateToastSeq = seq;
$t.stop(true, false).text(msg).show();
$t.delay(duration).fadeOut(160, () => {
if (el.__xdexImmediateToastSeq === seq) $t.remove();
});
return $t;
}
const Utils = {
// 逗号(中英)分隔,支持转义 \, \, \\
strToList(s) {
if (!s) return [];
const list = [], esc = ',,\\';
let cur = '';
for (let i = 0; i < s.length; i++) {
const ch = s[i];
if (ch === '\\' && i + 1 < s.length && esc.includes(s[i+1])) {
cur += s[++i];
} else if (ch === ',' || ch === ',') {
const t = cur.trim();
if (t) list.push(t);
cur = '';
} else cur += ch;
}
const t = cur.trim();
if (t) list.push(t);
return [...new Set(list)];
},
cookieLegal: s => /^[A-Za-z0-9]{3,8}$/.test(s),
cookieMatch: (cid,p) => cid.toLowerCase().includes(p.toLowerCase()),
firstHit(txt,list) {
return list.find(k=>txt.toLowerCase().includes(k.toLowerCase()))||null;
},
collapse($elem, hint) {
if (!$elem.length || $elem.data('xdex-collapsed')) return;
// 整表单不要在浮窗内被 early pass 再折成『回复/发串』按钮;
// 但 .collapse-wrapper(标题/名称/E-mail 可选项)即使已在 .qp-body 内也必须允许折叠
if ($elem.hasClass('qp-reply-form')) return;
if ($elem.closest('.qp-body').length && !$elem.hasClass('collapse-wrapper')) return;
const $icons = $elem.find('.h-threads-item-reply-icon');
let nums = '';
if ($icons.length) {
const f = $icons.first().text();
const l = $icons.last().text();
nums = $icons.length>1 ? `${f}-${l} ` : `${f} `;
}
const cap = `${nums}${hint}`;
const $ph = $(`
${cap}(点击展开)
`);
$elem.before($ph).hide().data('xdex-collapsed',true);
$elem.addClass('xdex-generic-collapsed'); // ★ 标记为公用折叠,以免触发板块页长串折叠/收起
$ph.on('click',()=>{
if($elem.is(':visible')){
$elem.hide(); $ph.html(`${cap}(点击展开)`);
} else {
$elem.show(); $ph.text('点击折叠');
}
});
return $ph;
},
// ===== 引用串优化缓存相关 =====
quoteCache: {},
getQuoteFromCache(id) {
return this.quoteCache[id] || GM_getValue('quote_' + id, null);
},
saveQuoteToCache(id, html) {
this.quoteCache[id] = html;
GM_setValue('quote_' + id, html);
}
};
// XDex 门面:设置面板经 spUi/spData/spUpdate 调用,便于实现迁出 tag 0
const XDex = {
ui: Object.create(null),
data: Object.create(null),
update: Object.create(null)
};
function xdexCall(bucket, name, args) {
const table = XDex[bucket];
const fn = table && table[name];
if (typeof fn !== 'function') {
throw new Error('[XDex.' + bucket + '] missing ' + name);
}
return fn.apply(null, args);
}
function spUi(name) { return xdexCall('ui', name, Array.prototype.slice.call(arguments, 1)); }
function spData(name) { return xdexCall('data', name, Array.prototype.slice.call(arguments, 1)); }
function spUpdate(name) { return xdexCall('update', name, Array.prototype.slice.call(arguments, 1)); }
/* --------------------------------------------------
* tag 1. 设置面板
* -------------------------------------------------- */
// 双刷新支持:如果上一次保存设置时要求执行第二次重载(localStorage 标记),
// 则在页面加载时触发第二次重载并清理标记。
try {
const _flag = localStorage.getItem('myScriptSettings_doSecondReload');
if (_flag === '1') {
localStorage.removeItem('myScriptSettings_doSecondReload');
console.log('[Settings] detected second-reload flag, performing second reload');
// 延迟短暂时间以让页面先完成初始化任务,再执行第二次重载
setTimeout(() => { try { location.reload(); } catch (e) { console.warn(e); } }, 200);
}
} catch (e) { /* ignore */ }
const SETTINGS_BUTTON_ICON_32 = 'data:image/png;base64,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';
const SETTINGS_BUTTON_ICON_64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAHVVJREFUeJztewl0G/W9LkmA5raldKX0Ao+errzS0ntvOa/tLfRyeb20r9DYgUIuSUggkA0SAiSQFEISIie2yeJ4i7d4je14X0eWN1m7ZHmXJUuWNNo1iySvCSkJTfJ7v/9IdqR4ycI9r+edc+ec72hGM575f99vHyW33fbf239v/99urpoVS/7ea7jpzd+fsYQZTl/mM6bdETBm3DIBuib+cXt1fC9dHb/GURN/+3/lGv9LN0tP8hLWkvIgZy96kXM1Slh3i4P1Sj/hvLJPOU/nGOduMXJuSsK7GpJZW8lz/Gjmv4Rs6Xcsdk8k/UMk34QAxEVEPX733f9HlG5scxuOLWPNKfdy1pOPs562DpY1TLMhBtjxMcT4VYyFgA1xwAacwHIj51mfjubtp0Qhe/pPnOaMOfdFol9AwhmI6YgABH/D7x/9O9CcuwVGU28PWFN/zNvyPuacjS2sS9zNcqYL7Fgwlvi8QHGISExPgHPVVwdsGT+/9v7o+l9CwlURy88IoCPf/z34xmwBa9odnL04j3G1nmdYCzCcDdigHxjeAV7cd+J3TsYCLnYUvH48z9N43hf2gmvFCPHAemTneFvO20Fb+j9EPwfJfgEt/iQSVyKGEE/MnKuyx99RaY+/Dz9XIRJxf+3nJkbvjruD3h3/CP1e/I/wc8H45K3pTzM+/UWGNQOB362EXloGrbQGGu1aaDDJQaJrAaq/A7qUzdBpkIHCoYYhpxxcHh0wATfCAYx/EMWzojAB9Ia+i7w9f9eYPf0L1z4Pid+H+CmKsbzWEb8Myf4BkYtQI84jADFS5Yq79WqBpB+yvxe/E/EWYjcKsJXePbf8BO3pX2GdDSVk4X4k4GOGQWbvgiZ9O5zpU0FZnwaqVDJoaJNAQ3srNLZKoFrVBRUGFVSNqkA8imLZZOBwydEz7CiABfOCC0jocF75CG/P/VXAnr5svjVW2VcuRaJpiIEI8SsR8gQXquj4B2+JPBL+AWIv4p8EMfY8QwR5AkXIdO1ZOSsCYz7xFdZRs4Xl7eeJ9fy8E7T97VCllkGtvBMqjSqoo9UgcfRAl00HapMSlIMykJhQFHs3nDFooEYphdquDqhTdIDe3AUOEkLsSDg5Ek/wq31uW+mhkq6kBxOpg0uj11lLP78kYnVYAAerHStvvlQi8ScQK5DwrPvR78Utxe9Ujt3xs3HJ2Ip+w3o6u9igFxi0PO0fgRZtG1R0K6Gltx26R8VgdbbhOQOwPOYFzAksWtiNYg27VCB3aqHFqIQ6VUfYQzQYIhg2Ho7GcOjFfMAi/ODxyC509aSeONV5+NEkSvTl6LWilTcuIkAL4ua9AIn+b8RTjihrR75vQ1HuIvuJlOj7o9ZaOcPbLjG+Hly0DbQODVQY1aC1SsDhHQA/fs8GPfNn/jEsgSich7PDkA2FGJJB6YAOygc1ILepUCwMB9aE14YE8VwO8WX1QLatuCspD5/91VkB7HFfRpK+BQTwIt6+aQGQ5E/R5Xcivh/13fdRAAlaYBkuYEt62yEbw/SBP0AD7WgFs98IraMSMNFtQnZnOEyIeO76JTAMPzcKNUi8yNgLjZIW0AzVA8saw/kAz3N+/QWGrjnvHC290qD+WJUiSfj3mbUhyYwFBOAq6fi/3LwH7BbcfRXiL4g/IZ5HHGw9selHSH5fslh0smcou86Pbm82d4J5pBNcbgXY3BrBbUmZY3x9kcXfSB8QBol/BYpQq+mCaoUU+gYlmFQHwz1CwPkZ72l5Gxulxx0jqe/r+o9VlMqStiaKRaT8PYOYjpB2IjoRf8FyeG+t/9mb5i9s7t3xS9AD7kXijyF+WZX75s+QfB1i1+DgsSdZd7vMPloNSm0dWEaUSFgvZG4Cv7cnTD7oBk7oAmfcnl9cBDzvxb9TY2UgOaFZ0QYGazMQoUn+4LzSap7OfzixWbQsSSz6H7iWFMSxYsO6B5HsAST9AeJRzAvfqqY/Rwm8dsOHfBkhijzwXtaSvp1xtXmMw+Ug7WvF+JSD36MIk8AGhwl4wrGP4EK+qCaHtL6OhQUIefH8KDZNJmgdaoXyXjW0GyVg96jD9/IqGJwd4iJrWoL4n4jGJPHBTeWWF4gI37hlknT85m/Z4zdvQRxCPOaI33x75EE/RBxC7CDH43TG11hnY5vfXn3F4WgAj38IS5Ue/E6JkLiEzE3IkIaGlDJCKnoO4EfDLo3JjQs653oFJkeOxzLIGkA5IoWmdjHo+yWCAKQ8Wu2thqOS/IdEzQVCSUxsPEhy0nGSlxDf/DwCrELiEwhAtODxd5KbRLcdEYt24Y1PI+4n12F7+gjjaBjx2htwQYawVZl+FKAVS1dfuJHB+s37upEILRDkZkSZ6fvJNYLF/cAF7OF6H+MJKBA7CE6XGtSddaBUNQgVA4U+73F2fJYvzcsRUQUPJzQXLIkY6Z8RZxDP37IASDo9Qp7Aj/hXvOGj5fIk6eBgyvGANXMXay9NxrovZ9jhC2GLhFtfEuuMR0qmOqHmcyjE2ZATgjPNzLVWJj3BTG5AD+F4Y8RToj0hKNzPieE1YqgRcgrnkVTyrvpSTX/+uZOdpyQowg8jAixFPIOowPzwrVsV4GmEBRFEFObn7v0zuWEyJfr2hCPzbs6a9XtZf2n7gKnGzzprtayL8gm9Ox+JaeINpG4j+XNjPvjr5ASM8Zaw5YQQiAoD9IAgWjh8bjxc6wNW9AY6/F30kITEiaiMRwacm2rg6eKXeFv6keHhk4aM9nwxivDtGQ643v+FyEY8cCsC3IVuvw4/D/Rv2Easn4Y4MHMeH3QfQirtyT/BWXOP8t52tdDhhfjZBEcalwBWA0KeYCpgA35GAIFURAgsbRMBC4Q4YyRJhsUh4cBxpogQnquiobcwfhJm1EXsPkcCtvRs3paRc1qeV4treh/zQThfNQqeQBqlN0mluCVPiCj5W0Q64hGBvLhwOT7oY5G44AXGkvErjq7ScnTFWFgAfzgPCNZywyQufkaA8+M8hHjrlVkrj0XcHq8ZYwfg/EQQr7cCj6RnQ4IIhSHFovdwbA8KgiUw5BGe4/dqPmNs5V7OktrFj6aWGYazHsN11eD6no5a+9ORnPXDzyNAEuKpRIloScT6jyH5ZLLPWU4s451VH7OOxgkGuzfG1xu2FCl9aKXpMe+sAARngw6hKxSmO9wXiCICXkWUUBxMcoNIeADP+WJyARekgWN0gB4HvEeCpbAVWNspJmBNP8qZU7+Ja7sLUYkifCOy9i9FKsLmln36f0T8DnH3DZPHVvfr+MflMyUFif8AH3AQ8VtybOs/fqdpoGyXm26nNYpysOGUN2N9MsBMobWiBSAI+buBdXcJZZFDiwsJj3jAOBtzXViIIWDcUox5NVpcC7RLBgp5CRgHqoF1tVwc98onzgYt/KSvPZG3pn0xYqDtuM5nEcsjIpD+oFB8QBuH5Fsk+/TbJPt65p8K7fGFP6HjC3fS8QXfi/zxOsTWw1gCIzdfh8hGCK+eGHP6i+6RQoXbUibKydx7SSqtizRAHiFZjSPBawX4BImO+3sg4O4EnrS2JM45A4QYPYZBKEqAAIyTGYIMQgEvDLu7ocvQCvWNJ8E2VMDz7s4LY96OsSkMiwmfkuftBc9E1vgI4jjinyIc7iAhXHW05SSSX4FYh0LsFu/rju0OHXH5S5H4ayjA4/i5Af9oOSKLKDhzjYgqTMAbbxaJ82/DavAdvaqgtaP99MGB7vwHc9J3e/JOpYcTIXkFho1KAHuCawX46+S4IMIUZv4p7PSCDLbKWDY5Zy1MMlo8PyZcN425gXNJhVdqDJbONrscDpWdgoyswxB0y6Ymmb4rk0z3lSnecGU6YLk85m5q9Y+cuAstT8IgH7Fq1pObElbXJnWUI/lvovXvQQHScf+3MQJY/k8+NkCFcegFm/HzKSS+gmTRq+QL7sHYyk4QFwrWF2VQdyJKEMKUWFbw/oaivA99dpsmEgbo4h4ljPvUQoKbK8SMR3AYFnr4dGpKsLogECbIMa8avWIAQ2kAzKY22HsqC/YeSoTW5hzdpKd+17gj948B+nTmmKfzHBFzGqtMyNVo5a3ZvzjUUriCeMHM2sX7dU+VpzYoqo9I/g0F+DHiARQhGfFbFGL5rAiuuIJljviCB8yr8kkJ+RDxUZQA/0Hia/Y4g3oEUYgQbjCiO/qAQZOy32mTTJIcQOaAALp4yNMJ50LuBQUQSiS68V8nJ2ePz2HjNI0JL8AO41yhhr7uekhIPg4yuXjCPNy0GbP+HSF300sTfl3fdGD0s7NB22UiwLhPByFnbVVXb+Hjh8UFJWisO1s+7F6CRPfXH+6aaP5IrcD904ijiFJEDuJHc3IB1s3lQieFOWCWsLgQk1/hI1EC5CJi3raG7Bl34ZRWwrrbLrEe2YVwk2PA7E8vKsCM28ckS2YIE58OfPZ6/OwFJshc9rhUH9PGkvs5W8kjU5zBMsX2uMc94uMTPsUkIT/NW1E812eTfpmiQn1aTfIAWvhbSLITAYhPEcOICiIK4jnEvXMFwO4JUYx4khwfpAq+jAKUiepPCQ1FQqZ4GZKfRvzjtX/LmFMf5NziIf9IWoVnOPUyg3NCCDu3c9eUxEU9AgcgH42jr7MNfB7tFZ+r65LPPzLsMOQ8RJ4x7m5JHvdKx8ZctesDdMUH417F36axcZrxItJzDIw0T6MAryLBr6MIq06W5IiSGw7XkKQ4h/DMdireMiPAzxPFoqOIn0es/wu8WfrMdQmZ1E+QvF2U2TrvnM3bc1/3mjLbZeLDkyxdi5ax3DD5v06MYZXAidKPLS8mU4e1dQwHqytO85k3Ohr2LAmMpn0haMt6KUifSQzQleUYPpeneTMm0F4gn4Q8EcLt6JwgDVuUUf+AKEPcMy/53HjHA3nx9F7EF/Gix0kOSKISvisIQBXEowiimWuR/ApE2UJCBm0Z3+ZGM94V14qmhrRZlwOYDElW/2TcDxM41Ezj6DsfeZIsp/F8wNcjlD6PSwfm4UYwDYvBOlSYxI9m/Hrc0/LxNG+ynAvSn05ikiTihnPIOO6PYgiEPW3cr/HjuktEzfnh1rgq5YnEuiPFSWLRD+ZdNBL/aW48LUUh7hY6P0q0EwW4JyLAa5hQZt+nIfmNogzx/mvvwVnSlmKC+s3IYKlF2916RaHpgOqyw6CQJABVmwDi2kSg0a0DrHFe8hPYPJ3DChAkXSVjAnq0Ewx9Z6BdpQO5VgYOY8E5jPlPMMYvBh3VZt5aIEP3nzw/MTabTGcqzjTXf+5wS8EZFOBrwpoPaJsPHRNbEpsSfraQ4W6ren5qxl2eEYYISvS1iABvoAfsihJgBwogvBSh9cfJz2HfGDWcfn14oLoNiV8gC76KbpBpFNDT0wx6PQW8W3ZxGhsk4qpksaRCEMtNY9zPJMMQHjusEjAO1kOHSjt7ry7hPk1u61DRr93D4R9IUIgnJv1aHWmiJvxXhy/ibZQury2rrvzHCfv0O0WYBBNESufh9Nrr/4CKxP+E2I7V4KsRAV5HD3g3SoDtiHf40bQvuk25rxj6qxsU2q5PO9VqiCV/FeRcTw8FQwPiy36HCs6iCFM4+2PWRiECMd4wht3hiKEe+nvr5tyHCNKtFw+jCE+xlrTlvDV1+Zi7YdW5ED1NKkH0fUaHSvw1ZYXihP36ABEAMSXa3/2HGxGATFE7EF+PCPCqKDYEXsNE+BE9nP+WvoeyIvkLCxGPhlrXAUqdDKwWrdDsfCK8KxiPCoOQgAl05eHBBkJ03vtI1apLfb0NSquh6DmynpCj6L4Jbwc15lXOltWQzwjpKR9cSjmSfPFYovpKRIDLiHduRIAnEbsR90UE+CMKkBglwFNZxdW+ga7yv0W76PXQpZYL4dA3oJtNVtEg34WFYcBiqAAVCra4oO2f2YeLXkRPvNvSn/OolEqCjpYzkHJ0P6zf8Co8vWI1vP3mNjh5goKIAIieftFe7dKF2YcF+DV5AYLT4PcjAvxLAlWYHk/Rd8dT9oc2nulbvydf6unuPD0lVSlviDwJAY1OAgqtAoYNSmyO5naHZ7FShNvhCfDZKEDvuu59B/oaBlGE1cM9uc9v27YJ1qx5GVbEr4U/Ivk1f34Fdm97HbLTSv92VQD9ecT91xPgYUQq4hfk+IC49Cu7qMZSJJ+PSF7ZbN+57bQuWN1ebVCrWm9IgO5uCpOaGMdbI0ywpjntMRmQsK2dPZ7ESjAyLL2+sCrVFY283t9cVyRPOPgWvLLqVXh99SbYsXYL7Fm/Dfa9th0aio6YkbR1RoSEfT0HrifAvZH3AL+Pa6ZvR8vveZEy+N4Utz0WFQa5ZWVlWQPqM4xMLVt0kTKNHHyObsG1Z1z92hAY8yhjjqdwmLKYFNcVgKqnoDUt9UplyenPqDMi58ENO2DfK29C0sZ34I01m2H9CxsgO/29T5JOVr+TkNQ54wXjon3dVztCtOrDSHL2l97E5oO3I/mSj6ij7+C5XXju0FuUeDuGwo5nxfY74yjHV/eckv5bck5TjUV2kupWNV1cbJFaFQVjzt7ZeZ+Uv+gJkbj+tR4R8g2DYaBtwXu2KjTQ3NwKHTkpPllZtjg5u7HEpDlStOq59bDzpdfhnZe2CjngT3FrYO8Hb8CpjsTKw6XZ5Ui+P5ILfhctwElEHhLdgJ+/wc9/3US1lq+n1AN4vBWPv7aRUt2/iVIaV1L2Ivwum4TD5upea7vytNSoLBhfVIDOQhh3DsRk/OhxmAwxscnQA3ZlOXTrmue9n1iqgPr8YpDmp07oatLWHc2t340e+UrAlrbjhRfXQRzmgD+vXAd/+NOLEP/ntZCVtvtC6qlK66ETTYeR/GZEEAVIwVBYGhGA/iUSWo84gPsnEMdXU715b1I1g89Slnvw+Gd4LhFF0KMImbj/u/+kTM/kaST9MpN4YlRbclGh6Zp3saRKNJYnw7hrMEIu1tLngq6YcCD7nKEVZA0ZoNR2zrlfm1ID9UXFoKjOAmNXujqzsObJyGT66Bid/pOSU/vO79q5FTZveQ32f7gDyoqSoEed7U4qPqVIEKleRuLYFPUMIhoT9vXeHRHAMWdCSqIOLj1MJSRtoGQiJJyL+A+RuCAOUdCqOfm41lRh6nWqhBegPGcDQ0cZdKnmxqxaK4GEvTuAt2kEtz8bcggJb8bS01GvzaaYUdA3Z8HhD9+ChoaiWKu3d0FTZQV0nM4Dr7ETnB4V9DqkIO+rmmyQVlOW4RyhZE9zg5Pk/eInQoPFhxsiW8ulWsXx4+aB00+ODJT+u8eY/s/6ztIvHdyrmw2Bo2jl38c308LbnnixYwl+9721VPfxVyi5chVl+B6KtPQAVXLvsdbCd6n+crvRIQcOM3UgxEJgfAycvfXQJ62eI4C8qwze2LQJRnWV2KqGfyghb4eFfl2YEiN9PDsC3Y0n4c03NsPW1zeCTN05G+vlZVXQWZoLFp0Y/K5h4XkEfNADVq8eBuzUefVobafPkrlimhu+eG2YeR1qLL/tU0qtdEyhlYYsshw7Zz7xRFQOoHciMpB0MeIY7qchcJ8+8gGVVXmISnwQBfjqs5R1//tSlUY10niFC7iAx+QVCPlwMePA2PVgls/1AnFTLrz88gbQi/Ngwt0vkA+PrOgB44wwB4x7dGDvKoIP390Occ+thSMf78XQ0YC4Uw51BcWgqckB+6AMON4DgbGQ8LwZ8CE/eDkLmJztl5SWOgPOFFeuFcDv7IlZk0mSC4wpZXWMy8c1OEkofBHxXcR34ihaGCP3Uye2bKTaGldS1tNHlPLtvSPVMjczEn64IIA3vJixADCOHhhUx/bvNZXH4ZmVa6Ay7UPgzao5Lz4YkwyaCw/D2nUvC83LwYPvQiNVDfVl5dBekgXOoXbgx4IxpGOA5xjOBRZsr7XoIWfnGbV5T1/MmiwtGcCMpDwV5QFzcwDZVoodSzEZbttAdXlfpaRbBk3FG0fs4mku6I0IYL8qADlGCzn6arBXv9od1lYeE7Jx8gdvw4i84mr2Jz+IjKqgsfAQbHz1VeGaVWvWw5kzWSCurwZFRQ64DVK0MLcweQwDj9cMRpMaew0lunmnkFfmCtAfk5TtrSeAG019OLoMzpsDEG8hPt5LZZ7AhJgxYqnQuL09kRgkAlhxgd6YBbGeARhQVGLrqwo3Kg3pArnVa9dDc94hYQCaYixgVZVD8v63gZQtcp4gNWUfNBbngbIyG1ifRbDuQuR5FNCJpVXVrcRnaYRnabpbUVj/3PeLvqsCkJHa0ZpyJWBLFypA0Jb2m9sE8hRdiWQDCCPu2xAWxL6VEiv51xf/kNeekD1qb8UH87OL4Nh+jEuL4P6zi5sYB2dPBfTKwqGgUxXD758JE9z33nZwqXNAV58EWzdvnCVO8M47W6CjLhv6GrOAwZK5sNWRfNAPRmOXMFhFu7YO2+1rR+vw26He2WuU2g6gm06cZa3vL/GbTixjhk8ULJoDyDagTLjDYMg5TP7lZ/RCOK8cvcA5Z4GMYwCs8lJQqKQ40DRDVeEHQoIrTN8DR0Vvw5qXXo4hv2nzayA5cwiUNbnA+rGyjIcWsTyL5NVkJJ5TcfqwEs28HYrJNZwBOtTaSFcqRgFSteP2jCW20VNrXd1phvnCP2ajB4/90mwuVcdYenwCOE97pAxes1CMW/+oBgYVVYKVOhsOgqn6GJjrj8Pe3W/AyufXzpJ/EUOjNG0P9DZlInnrbHjNb3mP8PvjfOQJhvpr5u02zwZtOIqH85JO2gROaUqGw5r3U6mV6jUMFZkXJZ997NXbjIbs3Q5azMRmXx54X9eCC+YYGuyqMiHmqOpDkC7aBflH90BbxQHIT90tZH0iwIG/bIe+phScEocWJU+8wunQQ3fv/B0ngXGwat6XrZ+MeUGuCQ9suo4G8Pac2Nlrr86psyk+7TVXtC0qgLk/9X7zSJnWy5hireHXQgAtsliskrJoVhRASXEKPB2/WiD8n5jpEz/aAS1V+0F04E2g+4qBZWyL3mcm6ZnMHaDSty8ogHmoCj6dmpwrAg5hmm7yd1rQK2qg11rJF9v7oMkmhQFbRcKiAhiHsh+3060Mw7uirB8A3ttx3UUHcNFuTT401GTGZPs/xq2GDz/YBkOadGC9fdex/NUKY/dgrTe1gLRbFgmD2LdRpsEFBED09lIg13WBfKgZSuw9UEL3Q6+96ZLDdnrz4h5gyt/r9g9ejilJLGbp4NzkNx9YnxH07WmweeuWWQGefeElqCo7gn3D6A2Sv+oFBrcOKke1IMYqIMVxWdMtAZWuHWNcAQO9dejurDBrkH6ANFpjzAC4sHRLza1Qa1FBkb0f8u2DoMc5xufpcrPOhqcWJD+sPrTcPFrr4QLuWOuzfTe8aAI3uu6RQ7sF4hs3b4HqyhzgAouHz8JexUKLXQWn6CHIow2QTw/CaVsPVNlUUGeTQ4NVBrX4WWVTI9m+2evIZwkeSxzd4CReR/7lmVd92mfJ+/GCAlj6jv/MQrddicn+2PuTf8dzM4tmPCNAnTkCm7H2l5RkgtdnvjXyEZhcMmi0K6Hc3o3EDALBhUDOF9t7oYHWg8KpB7c/snbyL9Q9KpHHlPntecnLWt6/vc9YeNjju8ba6Pp8wHGTVgtgi9wEjeXHwcfcWOgsfC8vVh8VitiNk2Av6B0ykNk7QGLtgGZrJzRau4CySqHV1gUKWoWujrnBZwAv7xBCaDacgu7PGLfsRUt/7vw/lroNxx/vM5Xq5/TiZAAiv93fzKJxgvP0NYJLW/z5yJMRmNFHPofQEDOJeUJ4BulSFx2covMJT3t9topfzUt+tDvpTr2pLMtil5yd88c4zgYC1y9bMQ8LsuA1UeAxNn8+8tjRkflDuCd6IY9j8K3ej2MG5S7D8fnd32LIfEg/Ujns8A/N44I4//PGGyePscZgvLr7ysGlL4HF2tzFRUTX581C7M6S4Mzzd6I3AL+jMbFXJpr/BxKtsWzFsI2a9nH2edwZEyLbe4OuHwDPMAUuTTEOQXn4iYOOQ3VriyahJ+SeqLKJ4clxI7cgphu8Izmz/3vi/wLKEJjy325E2wAAAABJRU5ErkJggg==';
function installSettingPanelEasterEgg(root = document) {
const scope = root && root.querySelector ? root : document;
const title = scope.querySelector('.xdex-setting-title-easter-egg');
if (!title || title.__xdexSettingEasterEggBound) return;
title.__xdexSettingEasterEggBound = true;
const CLICK_WINDOW_MS = 3000;
const REQUIRED_CLICKS = 3;
const EASTER_EGG_TID = '68811442';
let clicks = [];
title.addEventListener('click', () => {
const now = Date.now();
clicks = clicks.filter(t => now - t <= CLICK_WINDOW_MS);
clicks.push(now);
if (clicks.length < REQUIRED_CLICKS) return;
clicks = [];
try {
if (typeof enableQuotePreview === 'function') enableQuotePreview();
if (typeof window.__xdexOpenQuoteByTid === 'function') {
window.__xdexOpenQuoteByTid(EASTER_EGG_TID, { fromPOImage: true });
}
} catch (e) {
console.warn('[X岛-EX] setting panel easter egg failed', e);
}
});
}
const SettingPanel = {
key: 'myScriptSettings',
defaults: {
enableCookieSwitch: true,
enableCookieConfirm: false, // 发送前确认饼干
disableWatermark: true,
// note: `duplicatePagination` 已弃用,使用 `enablePaginationDuplication`
enablePaginationDuplication: true,
updatePreviewCookie: true,
hideEmptyTitleEmail: true,
enableExternalImagePreview: true, // 外部图床显示
enableAutoCookieRefresh: true,
enableAutoCookieRefreshToast: false,
interceptReplyFormUnvcode: true, // 拦截回复中间页--unvcode
interceptReplyFormU200B: true,
interceptReplyFormAutoCompress: true,
enableSeamlessPaging: true,
enableAutoSeamlessPaging: true,
enableHDImageAndLayoutFix: true, // 启用高清图片链接
enableLinkBlank: true, // 串页新标签打开
enableAutoUrlLinkify: true,
enableQuotePreview: true, // 优化引用弹窗
enableUpdateCheck: true,
enableImageContextMenu: true,
enableImageHideMode: true, // 图片隐藏/无图模式
applyImageHideMode: 'default', // default | blur | noimage | tips
enableDraft: true,
timeDisplayMode: 'relative', // relative | exact
extendQuote: true, // 拓展引用格式
enablePostExpandAll: true, // 默认展开板块页长串
kaomojiSort: 'default', // 颜文字排序:default | freq | recent
toggleSidebar: false, // 侧边栏收起功能
postAfterAction: 'jump', // 发串后:jump=新标签页打开 / refresh=刷新页面回板块第一页
disableAutoQuote: true, // 关闭引用:阻止URL中?r=参数自动插入引用号
threadCookieWhitelistGroups: [],
threadCookieWhitelistDisplayMode: 'fold', // 只看饼干:fold | hide | column
poAnnotationSideDisplayMode: 'collapse', // 分栏侧栏:collapse | expand
replyModeDefault: '回复', // 板块页默认模式:发串/回复
replyExtraDefault: '临时', // 板块/时间线默认额外模式:临时/连续
markedGroups: [],
blockedCookies: [],
blockedKeywords: [],
favoriteThreads: [],
subscriptionFeeds: [], // [{desc, uuid}, ...]
blockDisplayMode: 'hide' // fold = 折叠 | hide = 隐藏
},
state: {},
// JSONC 解析(支持 // 注释和尾随逗号)
parseJSONC(str) {
const cleaned = str
.replace(/\/\/.*$/gm, '')
.replace(/,(\s*[}\]])/g, '$1');
return JSON.parse(cleaned);
},
// 导出为 JSONC 字符串
buildJSONC(state) {
const lines = [
'// X岛-EX 配置文件',
'// 支持 // 注释和尾随逗号',
'{',
' "_meta": {',
` "version": "${typeof VERSION !== 'undefined' ? VERSION : 'unknown'}",`,
` "exportedAt": "${new Date().toISOString()}",`,
' "source": "nmbxd-EX"',
' },',
' "settings": {',
];
const entries = Object.entries(state);
entries.forEach(([key, val], i) => {
const json = JSON.stringify(val, null, 2);
if (json.includes('\n')) {
const indented = json.split('\n').map((line, li) => li === 0 ? line : ' ' + line).join('\n');
lines.push(` ${JSON.stringify(key)}: ${indented}${i < entries.length - 1 ? ',' : ''}`);
} else {
lines.push(` ${JSON.stringify(key)}: ${json}${i < entries.length - 1 ? ',' : ''}`);
}
});
lines.push(' },');
lines.push('}');
return lines.join('\n');
},
// 校验导入的配置
validateImport(incoming) {
if (typeof incoming !== 'object' || incoming === null) return { valid: false, error: '配置内容无效' };
const defaults = this.defaults;
const validated = {};
let skipped = 0;
for (const [key, val] of Object.entries(incoming)) {
if (!(key in defaults)) continue;
if (key === 'blockedKeywords') {
if (typeof val !== 'string' && !Array.isArray(val)) { skipped++; continue; }
validated[key] = spData('normalizeBlockedKeywordGroups', val);
continue;
}
if (typeof val !== typeof defaults[key]) { skipped++; continue; }
if (Array.isArray(defaults[key]) && !Array.isArray(val)) { skipped++; continue; }
validated[key] = val;
}
return { valid: true, merged: Object.assign({}, defaults, validated), skipped };
},
// 从文本导入(校验 + 暂存)
importFromText(text) {
let parsed;
try {
parsed = this.parseJSONC(text);
} catch (e) {
toast('配置文件格式错误'); return;
}
const incoming = parsed.settings || parsed;
const result = this.validateImport(incoming);
if (!result.valid) { toast(result.error); return; }
this.__pendingImport = result.merged;
const msg = result.skipped > 0
? `格式正确(${result.skipped} 个字段已跳过),请点击[应用]`
: '格式正确,请点击[应用]';
toast(msg);
const btn = document.getElementById('btn_xdex_import_export');
if (btn) btn.classList.remove('xdex-inv');
},
// 导出到剪贴板
async exportToClipboard() {
try {
const jsonc = this.buildJSONC(this.state);
await navigator.clipboard.writeText(jsonc);
toast('配置已复制到剪贴板');
} catch (e) {
toast('复制失败,请重试');
}
},
// 导出为文件
exportToFile() {
const jsonc = this.buildJSONC(this.state);
const blob = new Blob([jsonc], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `x岛-ex-settings-v${typeof VERSION !== 'undefined' ? VERSION : 'unknown'}.jsonc`;
a.click();
URL.revokeObjectURL(a.href);
toast('配置已导出');
},
// 从剪贴板导入
async importFromClipboard() {
try {
const text = await navigator.clipboard.readText();
if (!text) { toast('剪贴板为空'); return; }
this.importFromText(text);
} catch (e) {
toast('无法读取剪贴板,请先点击页面后再试');
}
},
// 从文件导入
importFromFile() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,.jsonc,.txt';
input.onchange = () => {
if (!input.files || !input.files[0]) return;
const reader = new FileReader();
reader.onload = () => this.importFromText(reader.result);
reader.readAsText(input.files[0]);
};
input.click();
},
syncAuxiliaryControls() {
const whitelistModeSelect = document.getElementById('sp_threadCookieWhitelistDisplayMode');
if (whitelistModeSelect) {
whitelistModeSelect.value = (this.state && this.state.threadCookieWhitelistDisplayMode) || 'fold';
}
const poSideModeSelect = document.getElementById('sp_poAnnotationSideDisplayMode');
if (poSideModeSelect) {
poSideModeSelect.value = (this.state && this.state.poAnnotationSideDisplayMode) || 'collapse';
}
const draftSelect = document.getElementById('sp_enableDraftMode');
if (draftSelect) {
const enabled = !!(this.state && this.state.enableDraft);
draftSelect.value = enabled ? 'off' : 'on';
}
const expandSelect = document.getElementById('sp_postExpandAllMode');
if (expandSelect) {
const enabled = !!(this.state && this.state.enablePostExpandAll);
expandSelect.value = enabled ? 'expand' : 'collapse';
}
const timeDisplaySelect = document.getElementById('sp_timeDisplayMode');
if (timeDisplaySelect) {
timeDisplaySelect.value = (this.state && this.state.timeDisplayMode === 'exact') ? 'exact' : 'relative';
}
const postAfterSelect = document.getElementById('sp_postAfterAction');
if (postAfterSelect) {
postAfterSelect.value = (this.state && this.state.postAfterAction === 'refresh') ? 'refresh' : 'jump';
}
},
init() {
const saved = GM_getValue(this.key, {});
const isFirstInit = Object.keys(saved).length === 0; // 判断是否首次初始化
console.log('init读取的原始数据:', JSON.stringify(saved));
this.state = Object.assign({}, this.defaults, saved);
if (this.state.timeDisplayMode !== 'exact') this.state.timeDisplayMode = 'relative';
// 该功能为固定启用项:避免历史配置把它保存为 false 导致下拉无法生效
this.state.enableImageHideMode = true;
console.log('init合并后的state:', JSON.stringify(this.state));
// 兼容迁移:屏蔽饼干到组结构
this.state.markedGroups = spData('normalizeMarkedGroups', this.state.markedGroups);
this.state.blockedCookies = spData('normalizeBlockedGroups', this.state.blockedCookies);
this.state.blockedKeywords = spData('normalizeBlockedKeywordGroups', this.state.blockedKeywords);
this.state.favoriteThreads = spData('normalizeFavoriteThreads', this.state.favoriteThreads);
this.state.threadCookieWhitelistGroups = spData('normalizeThreadCookieWhitelistGroups', this.state.threadCookieWhitelistGroups);
// 清理废弃字段
const validKeys = Object.keys(this.defaults);
let needCleanup = false;
Object.keys(this.state).forEach(key => {
if (!validKeys.includes(key)) {
delete this.state[key];
needCleanup = true;
}
});
console.log('init清理后的state:', JSON.stringify(this.state));
// 只在首次初始化或需要清理废弃字段时才保存
if (isFirstInit || needCleanup) {
console.log('首次初始化或需要清理,执行保存');
GM_setValue(this.key, this.state);
}
this.render();
GM_addValueChangeListener(this.key,(k,ov,nv,remote)=>{
if(remote){
this.state = Object.assign({}, this.defaults, nv);
this.state.markedGroups = spData('normalizeMarkedGroups', this.state.markedGroups);
this.state.blockedCookies = spData('normalizeBlockedGroups', this.state.blockedCookies);
this.state.blockedKeywords = spData('normalizeBlockedKeywordGroups', this.state.blockedKeywords);
this.state.favoriteThreads = spData('normalizeFavoriteThreads', this.state.favoriteThreads);
this.state.threadCookieWhitelistGroups = spData('normalizeThreadCookieWhitelistGroups', this.state.threadCookieWhitelistGroups);
if (this.state.timeDisplayMode !== 'exact') this.state.timeDisplayMode = 'relative';
this.syncInputs();
this.syncAuxiliaryControls();
try { renderFavoriteThreadsMenu(); } catch (e) {}
try { refreshFilterDisplay(this.state); } catch (e) {}
try { if (typeof window.__xdexApplyTimeDisplayMode === 'function') window.__xdexApplyTimeDisplayMode(document); } catch (e) {}
}
});
},
render() {
if (!$('#xdex-setting-style').length) {
$('head').append(`
`);
}
if (!$('#sp_btn').length) {
$('body').append(
$(``)
.on('click',()=>{
this.syncInputs();
this.syncAuxiliaryControls();
if (typeof window.__xdexSyncDarkReaderTheme === 'function') window.__xdexSyncDarkReaderTheme();
$('#sp_cover').fadeIn();
spUpdate('maybeShowPendingUpdateDialogOnPanelOpen');
})
);
spUpdate('updateSettingsButtonBadge', spUpdate('getUpdateCheckState'));
}
const fold = (id,title,ph) => `
`;
const checkboxItemStyle = 'width:50%; display:flex; align-items:center; gap:8px;';
const checkboxRowStyle = 'width:50%; display:flex; align-items:center; gap:8px;';
const quickReplyRowStyle = 'display:flex; align-items:center; gap:12px; margin:4px 0;';
const quickReplyColumnStyle = 'flex:1; display:flex; flex-direction:column; align-items:flex-start; gap:4px;';
const html = `
屏蔽关键词
默认逗号分隔关键词;开启“正则”后整组按正则匹配(不再逗号切分)
设置导入/导出
导入将覆盖当前全部配置,建议先导出备份
使用数据导入/导出
用于备份或迁移脚本/扩展运行产生的数据,支持按类别导出与导入。默认全选。
浏览历史
0 条
`;
$('#sp_cover').remove();
$('body').append(html);
syncFavoriteThreadsLinks();
installSettingPanelEasterEgg(document);
function setSettingsPanelModule(moduleName) {
const $nextView = $(`#sp_panel_views [data-sp-module-view="${moduleName}"]`);
const nextModule = $nextView.length ? moduleName : 'settings';
$('#sp_panel_tab_slot .sp_panel_tab').removeClass('active')
.filter(`[data-sp-module="${nextModule}"]`).addClass('active');
$('#sp_panel_views .sp_panel_module').removeClass('active').css('display', 'none');
const $activeModule = $('#sp_panel_views .sp_panel_module')
.filter(`[data-sp-module-view="${nextModule}"]`)
.addClass('active')
.css({ display: 'flex', flexDirection: 'column', flex: '1 1 auto', minHeight: (nextModule === 'history' || nextModule === 'posts' || nextModule === 'feeds') ? '300px' : '0' });
if (nextModule === 'history' || nextModule === 'posts' || nextModule === 'feeds') {
$activeModule.find('.sp_panel_content').css({ display: 'block', flex: '1 1 auto', minHeight: '300px', overflowY: 'auto', boxSizing: 'border-box' });
$activeModule.find('#sp_history_content,#sp_posts_content,#sp_feeds_content').css({ display: 'block' });
$activeModule.find('#sp_history_results,#sp_posts_results,#sp_feeds_results').css({ display: 'block', minHeight: '40px' });
}
$('#sp_panel_footer .sp_panel_links').show();
$('#sp_panel_footer .sp_panel_links a').toggle(nextModule === 'settings');
$('#sp_apply').toggle(nextModule === 'settings');
$('#sp_feeds_pager').toggle(nextModule === 'feeds');
updateThreadHistoryDebugState({
lastPanelModule: nextModule,
lastPanelState: {
requested: moduleName,
resolved: nextModule,
footerLinksVisible: $('#sp_panel_footer .sp_panel_links').is(':visible'),
applyVisible: $('#sp_apply').is(':visible'),
historyResults: !!document.getElementById('sp_history_results'),
historyResultsHeight: document.getElementById('sp_history_results')?.offsetHeight || 0,
historyModuleDisplay: getComputedStyle(document.getElementById('sp_module_history') || document.body).display,
historyContentDisplay: getComputedStyle(document.getElementById('sp_history_content') || document.body).display,
historyPanelContentHeight: document.querySelector('#sp_module_history .sp_panel_content')?.offsetHeight || 0,
at: new Date().toISOString()
}
});
logThreadHistory('panel module switched', threadHistoryDebugState.lastPanelState);
logThreadHistoryFlat('panel module switched flat', threadHistoryDebugState.lastPanelState);
}
$('#sp_panel_tab_slot').off('click', '[data-sp-module]').on('click', '[data-sp-module]', (e) => {
e.preventDefault();
logThreadHistory('panel tab clicked', { module: $(e.currentTarget).data('spModule') });
setSettingsPanelModule($(e.currentTarget).data('spModule'));
if ($(e.currentTarget).data('spModule') === 'history') renderThreadHistoryModuleSoon();
if ($(e.currentTarget).data('spModule') === 'posts') renderPostHistoryModuleSoon();
if ($(e.currentTarget).data('spModule') === 'feeds') renderSubscriptionFeedModule();
});
$('#sp_panel_tab_slot').off('mouseenter mouseleave', '.sp_panel_tab')
.on('mouseenter', '.sp_panel_tab', (e) => { $(e.currentTarget).addClass('is-hover'); })
.on('mouseleave', '.sp_panel_tab', (e) => { $(e.currentTarget).removeClass('is-hover'); });
setSettingsPanelModule('settings');
bindThreadHistoryModuleEvents();
bindThreadHistoryLiveSync();
bindPostHistoryModuleEvents();
bindPostHistoryLiveSync();
bindSubscriptionFeedModuleEvents();
// 浏览/发言历史改为首次进入对应 tab 时再渲染,避免 init 阶段双模块预构建卡顿
// 折叠头:统一控制
$('.sp_fold_head').off('click').on('click', function(){
const $head = $(this);
$head.next('.sp_fold_body').slideToggle(150);
const btns = ($head.data('btn') || '').split(',');
btns.forEach(sel => $(sel).toggleClass('xdex-inv'));
});
// 同步已有配置 & 默认折叠
this.syncInputs();
const reloadRequiredSettingKeys = [
'enableCookieSwitch',
'enableCookieConfirm',
'disableWatermark',
'enablePaginationDuplication',
'updatePreviewCookie',
'hideEmptyTitleEmail',
'enableExternalImagePreview',
'enableUpdateCheck',
'enableAutoCookieRefresh',
'enableAutoCookieRefreshToast',
'interceptReplyFormUnvcode',
'interceptReplyFormU200B',
'interceptReplyFormAutoCompress',
'enableSeamlessPaging',
'enableAutoSeamlessPaging',
'enableHDImageAndLayoutFix',
'enableImageContextMenu',
'enableLinkBlank',
'enableAutoUrlLinkify',
'enableQuotePreview',
'extendQuote',
'toggleSidebar',
'disableAutoQuote'
];
const collectReloadRequiredSettingsFromPanel = () => {
reloadRequiredSettingKeys.forEach(k => { this.state[k] = $('#sp_' + k).is(':checked'); });
// 固定启用:不受面板勾选状态影响
this.state.enableImageHideMode = true;
};
const saveReloadRequiredSettingsImmediately = () => {
collectReloadRequiredSettingsFromPanel();
try {
GM_setValue(this.key, this.state);
toast('设置已保存,刷新后生效', 900, { queue: false, key: 'settings-saved' });
} catch (e) {}
};
const reloadRequiredSettingSelector = reloadRequiredSettingKeys.map(k => '#sp_' + k).join(',');
$(reloadRequiredSettingSelector)
.off('change.xdexReloadSettingSave')
.on('change.xdexReloadSettingSave', saveReloadRequiredSettingsImmediately);
// 图片隐藏模式:即时切换并即时应用(无需点“应用更改”)
const applyImageHideModeImmediately = () => {
const mode = $('#sp_applyImageHideMode').val() || 'default';
// 固定启用,仅切换具体模式
this.state.enableImageHideMode = true;
this.state.applyImageHideMode = mode;
try { GM_setValue(this.key, this.state); } catch (e) {}
if (typeof applyImageHideMode === 'function') {
applyImageHideMode(mode, document);
}
};
$('#sp_enableImageHideMode').off('change').on('change', applyImageHideModeImmediately);
$('#sp_applyImageHideMode').off('change').on('change', applyImageHideModeImmediately);
// 设置面板内打开阅图:串内页可用;无图模式下右上角按钮隐藏时的备选入口
$('#sp_openImageViewer').off('click').on('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (typeof openImageViewerFromSettingsPanel === 'function') {
openImageViewerFromSettingsPanel();
} else if (typeof toast === 'function') {
toast('当前页面不支持阅图模式');
}
});
// 屏蔽显示模式:即时切换并即时生效(折叠/隐藏)
const applyBlockDisplayModeImmediately = () => {
const mode = $('#sp_blockDisplayMode').val() || 'fold';
this.state.blockDisplayMode = mode;
try { GM_setValue(this.key, this.state); } catch (e) {}
refreshFilterDisplay(this.state);
};
$('#sp_blockDisplayMode').off('change').on('change', applyBlockDisplayModeImmediately);
const applyThreadCookieWhitelistDisplayModeImmediately = () => {
this.state.threadCookieWhitelistDisplayMode = $('#sp_threadCookieWhitelistDisplayMode').val() || 'fold';
this.state.poAnnotationSideDisplayMode = $('#sp_poAnnotationSideDisplayMode').val() || 'collapse';
try { GM_setValue(this.key, this.state); } catch (e) {}
refreshFilterDisplay(this.state);
};
$('#sp_threadCookieWhitelistDisplayMode').off('change').on('change', applyThreadCookieWhitelistDisplayModeImmediately);
$('#sp_poAnnotationSideDisplayMode').off('change').on('change', applyThreadCookieWhitelistDisplayModeImmediately);
// 颜文字排序:即时切换并即时生效(无需点“应用更改”)
const applyKaomojiSortImmediately = () => {
const mode = $('#sp_kaomojiSort').val() || 'default';
this.state.kaomojiSort = mode;
try { GM_setValue(this.key, this.state); } catch (e) {}
// 与颜文字按钮右侧的快捷下拉实时同步
$('.sp_kaomojiSort_copy').val(mode);
document.querySelectorAll('#h-emot-select').forEach(sel => {
try {
sel.dispatchEvent(new Event('kaomoji:sort-changed'));
} catch (e) {}
});
// 广播排序模式变化,供其它复制下拉同步
try {
window.dispatchEvent(new CustomEvent('kaomoji:sort-mode-changed', { detail: { mode, source: 'settings' } }));
} catch (e) {}
};
$('#sp_kaomojiSort').off('change').on('change', applyKaomojiSortImmediately);
(function initPostExpandModeSelect() {
const sel = document.getElementById('sp_postExpandAllMode');
if (!sel) return;
sel.value = (SettingPanel.state && SettingPanel.state.enablePostExpandAll) ? 'expand' : 'collapse';
sel.addEventListener('change', (e) => {
e.stopPropagation();
const nextState = (sel.value || 'expand') === 'expand';
SettingPanel.state.enablePostExpandAll = nextState;
const items = document.querySelectorAll('.h-threads-item-index');
let anchor = null;
for (const item of items) {
const rect = item.getBoundingClientRect();
if (rect.top >= 0) { anchor = item; break; }
}
if (!anchor && items.length) anchor = items[items.length - 1];
const anchorTopBefore = anchor ? anchor.getBoundingClientRect().top : null;
items.forEach(item => {
const toggleBtn = item.querySelector('.h-threads-info .js-toggle-mode');
if (!toggleBtn) return;
const expanded = item.classList.contains('expanded');
if (nextState && !expanded) {
toggleBtn.click();
} else if (!nextState && expanded) {
toggleBtn.click();
}
});
if (!nextState && anchor && anchorTopBefore !== null) {
requestAnimationFrame(() => {
const anchorTopAfter = anchor.getBoundingClientRect().top;
const delta = anchorTopAfter - anchorTopBefore;
window.scrollBy({ top: delta, behavior: 'instant' });
});
}
try { GM_setValue(SettingPanel.key, SettingPanel.state); } catch (err) {}
toast(nextState ? '已展开长串' : '已折叠长串');
});
})();
(function initPostAfterActionSelect() {
const sel = document.getElementById('sp_postAfterAction');
if (!sel) return;
sel.value = (SettingPanel.state && SettingPanel.state.postAfterAction) || 'jump';
sel.addEventListener('change', (e) => {
e.stopPropagation();
const action = (sel.value || 'jump') === 'jump' ? 'jump' : 'refresh';
SettingPanel.state.postAfterAction = action;
try { GM_setValue(SettingPanel.key, SettingPanel.state); } catch (err) {}
toast(action === 'jump' ? '已切换为:发串后新标签页打开' : '已切换为:发串后刷新页面回顶部');
});
})();
(function initDraftModeSelect() {
const sel = document.getElementById('sp_enableDraftMode');
if (!sel) return;
sel.value = (SettingPanel.state && SettingPanel.state.enableDraft) ? 'off' : 'on';
sel.addEventListener('change', (e) => {
e.stopPropagation();
const enabled = (sel.value || 'off') === 'off';
SettingPanel.state.enableDraft = enabled;
try { GM_setValue(SettingPanel.key, SettingPanel.state); } catch (err) {}
if (!enabled) {
deleteAllDraftsSafe();
toast('已清除缓存草稿并关闭草稿功能');
} else {
try { migrateLegacyDraftIfNeeded(); } catch (err) {}
try { if (typeof 载入编辑 === 'function') 载入编辑(); } catch (err) {}
try { if (typeof 注册自动保存编辑 === 'function') 注册自动保存编辑(); } catch (err) {}
toast('已开启草稿缓存');
}
});
})();
(function initTimeDisplayModeSelect() {
const sel = document.getElementById('sp_timeDisplayMode');
if (!sel) return;
sel.value = (SettingPanel.state && SettingPanel.state.timeDisplayMode === 'exact') ? 'exact' : 'relative';
sel.addEventListener('change', (e) => {
e.stopPropagation();
const mode = (sel.value === 'exact') ? 'exact' : 'relative';
SettingPanel.state.timeDisplayMode = mode;
try { GM_setValue(SettingPanel.key, SettingPanel.state); } catch (err) {}
try { if (typeof window.__xdexApplyTimeDisplayMode === 'function') window.__xdexApplyTimeDisplayMode(document); } catch (err) {}
toast(mode === 'exact' ? '已切换为精确时间' : '已切换为相对时间');
});
})();
// 标记:新增组输入
$('#btn_group_marked').off('click').on('click', e=>{
e.stopPropagation();
const nextIndex = $('#marked-inputs-container .marked-row').length + 1;
$('#marked-inputs-container').append(
spUi('buildCookieGroupTwoFieldRowHtml', 'marked', nextIndex)
).find('.marked-desc-input').last().focus();
});
// 屏蔽:新增组输入
$('#btn_group_blocked').off('click').on('click', e=>{
e.stopPropagation();
const nextIndex = $('#blocked-inputs-container .blocked-row').length + 1;
$('#blocked-inputs-container').append(
spUi('buildCookieGroupTwoFieldRowHtml', 'blocked', nextIndex)
).find('.blocked-desc-input').last().focus();
});
$('#btn_group_threadCookieWhitelist').off('click').on('click', e=>{
e.stopPropagation();
const nextIndex = $('#thread-cookie-whitelist-inputs-container .thread-cookie-whitelist-row').length + 1;
$('#thread-cookie-whitelist-inputs-container').append(spUi('buildThreadCookieWhitelistRowHtml', nextIndex));
$('#thread-cookie-whitelist-inputs-container .thread-cookie-whitelist-row').last().find('.thread-cookie-whitelist-desc-input').focus();
});
$('#btn_group_blockedKeywords').off('click').on('click', e=>{
e.stopPropagation();
const nextIndex = $('#blocked-keyword-inputs-container .blocked-keyword-row').length + 1;
$('#blocked-keyword-inputs-container').append(spUi('buildBlockedKeywordGroupRowHtml', nextIndex));
$('#blocked-keyword-inputs-container .blocked-keyword-row').last().find('.blocked-keyword-input').focus();
});
$('#btn_group_favoriteThreads').off('click').on('click', e=>{
e.stopPropagation();
const nextIndex = $('#favorite-thread-inputs-container .favorite-thread-row').length + 1;
$('#favorite-thread-inputs-container').append(spUi('buildFavoriteThreadRowHtml', nextIndex));
$('#favorite-thread-inputs-container .favorite-thread-row').last().find('.favorite-thread-desc-input').focus();
});
$('#btn_group_subscriptionFeeds').off('click').on('click', e=>{
e.stopPropagation();
const nextIndex = $('#subscription-feed-inputs-container .subscription-feed-row').length + 1;
$('#subscription-feed-inputs-container').append(spUi('buildSubscriptionFeedRowHtml', nextIndex));
$('#subscription-feed-inputs-container .subscription-feed-row').last().find('.subscription-feed-desc-input').focus();
});
const saveMarkedGroups = ({ fromDelete = false } = {}) => {
const parsed = collectMarkedGroupsFromPanel();
if (!parsed) return false;
this.state.markedGroups = parsed;
GM_setValue(this.key, this.state);
this.syncInputs();
toast(fromDelete ? '已删除标记分组' : '标记分组已保存');
refreshFilterDisplay(this.state);
return true;
};
const saveBlockedGroups = ({ fromDelete = false } = {}) => {
const parsed = [];
let valid = true;
$('#blocked-inputs-container .blocked-row').each((idx, el)=>{
const $row = $(el);
const desc = ($row.find('.blocked-desc-input').val() || '').trim();
const cookies = Utils.strToList(($row.find('.blocked-cookies-input').val() || '').trim());
if (!desc && !cookies.length) return;
if (!spData('isValidDesc', desc)) { toast(`第${idx + 1}条备注过长`); valid=false; return false; }
if (!cookies.length) { toast(`第${idx + 1}条未指定饼干`); valid=false; return false; }
if (cookies.some(id=>!Utils.cookieLegal(id))) { toast(`第${idx + 1}条存在不合法饼干`); valid=false; return false; }
parsed.push({ desc, cookies });
});
if (!valid) return false;
this.state.blockedCookies = parsed;
GM_setValue(this.key, this.state);
this.syncInputs();
toast(fromDelete ? '已删除屏蔽分组' : '屏蔽分组已保存');
refreshFilterDisplay(this.state);
return true;
};
const saveThreadCookieWhitelistGroups = ({ fromDelete = false } = {}) => {
const parsed = [];
let valid = true;
$('#thread-cookie-whitelist-inputs-container .thread-cookie-whitelist-row').each((idx, el) => {
const $row = $(el);
const desc = ($row.find('.thread-cookie-whitelist-desc-input').val() || '').trim();
const threads = Utils.strToList(($row.find('.thread-cookie-whitelist-threads-input').val() || '').trim());
const cookies = Utils.strToList(($row.find('.thread-cookie-whitelist-cookies-input').val() || '').trim());
if (!desc && !threads.length && !cookies.length) return;
if (!spData('isValidDesc', desc)) { toast(`第${idx + 1}条备注过长`); valid = false; return false; }
if (!threads.length) { toast(`第${idx + 1}条未指定串号`); valid = false; return false; }
if (threads.some(id => !spData('isValidThreadId', id))) { toast(`第${idx + 1}条存在不合法串号`); valid = false; return false; }
if (!cookies.length) { toast(`第${idx + 1}条未指定饼干`); valid = false; return false; }
if (cookies.some(id => !Utils.cookieLegal(id))) { toast(`第${idx + 1}条存在不合法饼干`); valid = false; return false; }
parsed.push({ desc, threads, cookies, rowIndex: idx + 1 });
});
if (!valid) return false;
const { groups, mergeEvents } = spData('mergeThreadCookieWhitelistGroups', parsed);
this.state.threadCookieWhitelistGroups = groups;
GM_setValue(this.key, this.state);
mergeEvents.forEach((event) => {
if (event.desc) {
toast(`第${event.rowIndex}条(${event.desc})中的串号 ${event.threadId} 已与现有只看规则合并`);
} else {
toast(`第${event.rowIndex}条的串号 ${event.threadId}(${event.cookies.join(',')})已与现有只看规则合并`);
}
});
this.syncInputs();
toast(fromDelete ? '已删除只看饼干分组' : '只看饼干分组已保存');
refreshFilterDisplay(this.state);
return true;
};
const collectBlockedKeywordGroupsFromPanel = () => {
const parsed = [];
let valid = true;
$('#blocked-keyword-inputs-container .blocked-keyword-row').each((idx, el)=>{
const $row = $(el);
const rawValue = ($row.find('.blocked-keyword-input').val() || '').trim();
const regex = !!$row.find('.blocked-keyword-regex').prop('checked');
if (!rawValue) return;
if (regex) {
if (!spData('compileBlockedKeywordRegex', rawValue)) {
toast(`第${idx + 1}组正则无效,请检查表达式`);
valid = false;
return false;
}
parsed.push({ value: rawValue, regex: true });
return;
}
if (Utils.strToList(rawValue).length) parsed.push({ value: rawValue, regex: false });
});
return valid ? parsed : null;
};
const saveBlockedKeywordGroups = ({ fromDelete = false, fromToggle = false, toggleIndex = 0, toggleOn = false } = {}) => {
const parsed = collectBlockedKeywordGroupsFromPanel();
if (!parsed) return false;
this.state.blockedKeywords = parsed;
GM_setValue(this.key, this.state);
// 开关切换实时保存时避免 syncInputs 整表重绘打断当前行焦点
if (!fromToggle) this.syncInputs();
if (fromDelete) toast('已删除关键词分组');
else if (fromToggle) toast(`第${toggleIndex}组${toggleOn ? '开启' : '关闭'}正则`);
else toast('屏蔽关键词已保存');
refreshFilterDisplay(this.state);
return true;
};
const saveFavoriteThreads = ({ fromDelete = false } = {}) => {
const parsed = spUi('collectFavoriteThreadsFromPanel');
if (!parsed) return false;
this.state.favoriteThreads = parsed;
GM_setValue(this.key, this.state);
this.syncInputs();
renderFavoriteThreadsMenu();
toast(fromDelete ? '已删除常用串' : '常用串已保存');
return true;
};
$('#marked-inputs-container').off('click', '.marked-delete').on('click', '.marked-delete', (e) => {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).closest('.marked-row').remove();
saveMarkedGroups({ fromDelete: true });
return false;
});
$('#blocked-inputs-container').off('click', '.blocked-delete').on('click', '.blocked-delete', (e) => {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).closest('.blocked-row').remove();
saveBlockedGroups({ fromDelete: true });
return false;
});
$('#thread-cookie-whitelist-inputs-container').off('click', '.thread-cookie-whitelist-delete').on('click', '.thread-cookie-whitelist-delete', (e) => {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).closest('.thread-cookie-whitelist-row').remove();
saveThreadCookieWhitelistGroups({ fromDelete: true });
return false;
});
$('#blocked-keyword-inputs-container').off('click', '.blocked-keyword-delete').on('click', '.blocked-keyword-delete', (e) => {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).closest('.blocked-keyword-row').remove();
saveBlockedKeywordGroups({ fromDelete: true });
return false;
});
// 正则开关:切换 placeholder,并实时保存 + 刷新过滤(无需点保存)
$('#blocked-keyword-inputs-container').off('change.blockedKeywordRegex', '.blocked-keyword-regex').on('change.blockedKeywordRegex', '.blocked-keyword-regex', function () {
const $sw = $(this);
const $row = $sw.closest('.blocked-keyword-row');
const on = !!$sw.prop('checked');
const toggleIndex = $('#blocked-keyword-inputs-container .blocked-keyword-row').index($row) + 1;
$row.attr('data-regex', on ? '1' : '0');
$row.find('.blocked-keyword-input').attr('placeholder', spUi('getBlockedKeywordPlaceholder', on));
// 实时写入配置并 applyFilters;失败(如正则非法)则回滚开关
if (!saveBlockedKeywordGroups({ fromToggle: true, toggleIndex, toggleOn: on })) {
$sw.prop('checked', !on);
$row.attr('data-regex', !on ? '1' : '0');
$row.find('.blocked-keyword-input').attr('placeholder', spUi('getBlockedKeywordPlaceholder', !on));
}
});
$('#favorite-thread-inputs-container').off('click', '.favorite-thread-delete').on('click', '.favorite-thread-delete', (e) => {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).closest('.favorite-thread-row').remove();
saveFavoriteThreads({ fromDelete: true });
return false;
});
// 标记:保存
$('#btn_sp_marked').off('click').on('click', e=>{
e.stopPropagation();
saveMarkedGroups();
});
// 屏蔽:保存
$('#btn_sp_blocked').off('click').on('click', e=>{
e.stopPropagation();
saveBlockedGroups();
});
$('#btn_sp_blockedKeywords').off('click').on('click', e=>{
e.stopPropagation();
saveBlockedKeywordGroups();
});
$('#btn_sp_favoriteThreads').off('click').on('click', e=>{
e.stopPropagation();
saveFavoriteThreads();
});
$('#subscription-feed-inputs-container').off('click', '.subscription-feed-delete').on('click', '.subscription-feed-delete', (e) => {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).closest('.subscription-feed-row').remove();
saveSubscriptionFeeds({ fromDelete: true });
return false;
});
// 订阅号:保存
const saveSubscriptionFeeds = ({ fromDelete = false } = {}) => {
const parsed = spUi('collectSubscriptionFeedsFromPanel');
if (!parsed) return false;
this.state.subscriptionFeeds = parsed;
GM_setValue(this.key, this.state);
this.syncInputs();
toast(fromDelete ? '已删除订阅号' : '订阅号已保存');
return true;
};
$('#btn_sp_subscriptionFeeds').off('click').on('click', e=>{
e.stopPropagation();
saveSubscriptionFeeds();
});
$('#btn_sp_threadCookieWhitelist').off('click').on('click', e=>{
e.stopPropagation();
saveThreadCookieWhitelistGroups();
});
const closeMarkedColorPopovers = () => {
$('#marked-inputs-container .marked-color-popover').hide();
};
const getMarkedRowDefaultColor = ($row) => {
const rowIndex = Math.max($row.index(), 0);
return XDex.data.markColors[rowIndex % XDex.data.markColors.length];
};
const updateMarkedRowSwatch = ($row, draftColor) => {
const color = spData('normalizeHexColor', draftColor) || getMarkedRowDefaultColor($row);
$row.find('.marked-color-swatch').css('background', color).attr('data-default-color', color);
$row.find('.marked-color-preview').css('background', color);
};
const readMarkedColorInputValue = ($popover) => ($popover.find('.marked-color-input').val() || '').trim();
const readMarkedColorInputAsHex = ($popover) => {
const state = $popover.data('pickerState') || {};
const rawValue = readMarkedColorInputValue($popover);
if (!rawValue) return '';
if ((state.format || 'hex') === 'rgb') {
const rgb = spData('parseRgbColorString', rawValue);
return rgb ? spData('rgbToHex', rgb) : null;
}
const normalized = spData('normalizeHexColor', rawValue);
return normalized || null;
};
const setMarkedColorInputFromState = ($popover) => {
const state = $popover.data('pickerState');
if (!state) return;
const inputValue = state.inputEmpty
? ''
: (state.format === 'rgb' ? spData('formatRgbColor', spData('hexToRgb', state.hex)) : state.hex);
$popover.find('.marked-color-input').val(inputValue);
};
const updateMarkedColorFormatButtons = ($popover) => {
const state = $popover.data('pickerState') || {};
$popover.find('.marked-color-format').each((_, el) => {
const $btn = $(el);
const active = $btn.data('format') === (state.format || 'hex');
$btn.css({
border: active ? '1px solid #7da6bf' : '1px solid #a98f7a',
background: active ? '#66CCFF' : '#F0E0D6',
color: active ? '#fff' : '#6f5d50',
});
});
};
const renderMarkedColorPicker = ($row) => {
const $popover = $row.find('.marked-color-popover');
const state = $popover.data('pickerState');
if (!state) return;
const displayHex = spData('normalizeHexColor', state.hex) || state.defaultHex;
const displayHsv = spData('hexToHsv', displayHex);
const hueHex = spData('hsvToHex', state.hsv.h, 1, 1);
const $sv = $popover.find('.marked-color-sv');
const $svThumb = $popover.find('.marked-color-sv-thumb');
const $hueThumb = $popover.find('.marked-color-hue-thumb');
$sv.css('background', `linear-gradient(to top, #000 0%, transparent 100%), linear-gradient(to right, #fff 0%, ${hueHex} 100%)`);
$svThumb.css({ left: `${state.hsv.s * 100}%`, top: `${(1 - state.hsv.v) * 100}%` });
$hueThumb.css('left', `${(state.hsv.h / 360) * 100}%`);
$popover.find('.marked-color-preview').css('background', displayHex);
$popover.find('.marked-color-status').text(state.inputEmpty ? `默认色 ${state.defaultHex}` : displayHex);
$popover.find('.marked-color-default-hint').text(`默认 ${state.defaultHex}`);
setMarkedColorInputFromState($popover);
updateMarkedColorFormatButtons($popover);
updateMarkedRowSwatch($row, state.inputEmpty ? '' : displayHex);
};
const setMarkedColorPickerHex = ($row, nextHex, options = {}) => {
const $popover = $row.find('.marked-color-popover');
const state = $popover.data('pickerState');
if (!state) return;
const normalized = spData('normalizeHexColor', nextHex);
const allowEmpty = !!options.allowEmpty;
if (!normalized && !allowEmpty) return;
state.inputEmpty = !normalized;
state.hex = normalized || state.defaultHex;
state.hsv = spData('hexToHsv', state.hex);
$popover.data('pickerState', state);
renderMarkedColorPicker($row);
};
const setMarkedColorPickerFormat = ($row, format) => {
const $popover = $row.find('.marked-color-popover');
const state = $popover.data('pickerState');
if (!state) return;
state.format = format === 'rgb' ? 'rgb' : 'hex';
$popover.data('pickerState', state);
renderMarkedColorPicker($row);
};
const updateMarkedColorPickerFromPointer = ($row, areaType, clientX, clientY) => {
const $popover = $row.find('.marked-color-popover');
const state = $popover.data('pickerState');
if (!state) return;
if (areaType === 'sv') {
const rect = $popover.find('.marked-color-sv')[0].getBoundingClientRect();
const s = spData('clampColorChannel', (clientX - rect.left) / rect.width, 0, 1);
const v = spData('clampColorChannel', 1 - ((clientY - rect.top) / rect.height), 0, 1);
state.hsv.s = s;
state.hsv.v = v;
} else {
const rect = $popover.find('.marked-color-hue')[0].getBoundingClientRect();
state.hsv.h = spData('clampColorChannel', ((clientX - rect.left) / rect.width) * 360, 0, 360);
if (state.hsv.h === 360) state.hsv.h = 359.999;
}
state.inputEmpty = false;
state.hex = spData('hsvToHex', state.hsv.h, state.hsv.s, state.hsv.v);
$popover.data('pickerState', state);
renderMarkedColorPicker($row);
};
const positionMarkedColorPopover = ($row) => {
const $cell = $row.find('.marked-color-cell');
const $popover = $row.find('.marked-color-popover');
if (!$cell.length || !$popover.length) return;
$popover.css({ top: '', bottom: '' });
const cellRect = $cell[0].getBoundingClientRect();
const popRect = $popover[0].getBoundingClientRect();
const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
const gap = 6;
const spaceBelow = viewportHeight - cellRect.bottom - gap;
const spaceAbove = cellRect.top - gap;
const openAbove = spaceBelow < popRect.height && spaceAbove > spaceBelow;
const popoverHeight = popRect.height;
if (openAbove) {
let offsetBottom = cellRect.height + gap;
const topIfOpened = cellRect.bottom - offsetBottom - popoverHeight;
if (topIfOpened < 4) {
offsetBottom = Math.max(cellRect.bottom - popoverHeight - 4, gap);
}
$popover.css({ top: 'auto', bottom: `${offsetBottom}px` });
} else {
let offsetTop = cellRect.height + gap;
const overflow = cellRect.top + offsetTop + popoverHeight - viewportHeight + 4;
if (overflow > 0) {
offsetTop = Math.max(offsetTop - overflow, -cellRect.top + 4);
}
$popover.css({ top: `${offsetTop}px`, bottom: 'auto' });
}
};
const openMarkedColorPopover = ($row) => {
const $popover = $row.find('.marked-color-popover');
const storedColor = ($row.find('.marked-color-value').val() || '').trim();
const defaultHex = getMarkedRowDefaultColor($row);
const displayHex = spData('normalizeHexColor', storedColor) || defaultHex;
$popover.data('pickerState', {
format: 'hex',
defaultHex,
inputEmpty: !spData('normalizeHexColor', storedColor),
hex: displayHex,
hsv: spData('hexToHsv', displayHex),
});
renderMarkedColorPicker($row);
$popover.show();
positionMarkedColorPopover($row);
$popover.find('.marked-color-input').trigger('focus').trigger('select');
};
const beginMarkedColorDrag = ($row, areaType, startEvent) => {
const moveEvent = startEvent.type.indexOf('touch') === 0 ? 'touchmove.markedColorDrag' : 'mousemove.markedColorDrag';
const endEvent = startEvent.type.indexOf('touch') === 0 ? 'touchend.markedColorDrag touchcancel.markedColorDrag' : 'mouseup.markedColorDrag';
const getPoint = (evt) => {
const touch = evt.originalEvent && evt.originalEvent.touches && evt.originalEvent.touches[0];
const changedTouch = evt.originalEvent && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches[0];
return touch || changedTouch || evt;
};
const applyPointer = (evt) => {
const point = getPoint(evt);
updateMarkedColorPickerFromPointer($row, areaType, point.clientX, point.clientY);
};
applyPointer(startEvent);
$(document).off('.markedColorDrag').on(moveEvent, (evt) => {
evt.preventDefault();
applyPointer(evt);
}).on(endEvent, () => {
$(document).off('.markedColorDrag');
});
};
const collectMarkedGroupsFromPanel = () => {
const parsed = [];
let valid = true;
$('#marked-inputs-container .marked-row').each((idx, el)=>{
const $row = $(el);
const desc = ($row.find('.marked-desc-input').val() || '').trim();
const cookies = Utils.strToList(($row.find('.marked-cookies-input').val() || '').trim());
const rawColor = ($row.find('.marked-color-value').val() || '').trim();
if (!desc && !cookies.length) return;
if (!spData('isValidDesc', desc)) { toast(`第${idx + 1}条备注过长`); valid=false; return false; }
if (!cookies.length) { toast(`第${idx + 1}条未指定饼干`); valid=false; return false; }
if (cookies.some(id=>!Utils.cookieLegal(id))) { toast(`第${idx + 1}条存在不合法饼干`); valid=false; return false; }
if (rawColor && !spData('isValidHexColor', rawColor)) { toast(`第${idx + 1}条颜色格式无效,应为 #RRGGBB`); valid=false; return false; }
parsed.push({ desc, color: spData('normalizeHexColor', rawColor), cookies });
});
return valid ? parsed : null;
};
$(document).off('click.markedColorPopover').on('click.markedColorPopover', (e) => {
if ($(e.target).closest('#marked-inputs-container .marked-color-cell').length) return;
closeMarkedColorPopovers();
});
$('#marked-inputs-container').off('click', '.marked-color-swatch').on('click', '.marked-color-swatch', (e) => {
e.preventDefault();
e.stopPropagation();
const $row = $(e.currentTarget).closest('.marked-row');
const $popover = $row.find('.marked-color-popover');
const shouldOpen = !$popover.is(':visible');
closeMarkedColorPopovers();
if (!shouldOpen) return false;
openMarkedColorPopover($row);
return false;
});
$('#marked-inputs-container').off('input', '.marked-color-input').on('input', '.marked-color-input', (e) => {
const $row = $(e.currentTarget).closest('.marked-row');
const $popover = $row.find('.marked-color-popover');
const normalized = readMarkedColorInputAsHex($popover);
if (normalized === '') {
setMarkedColorPickerHex($row, '', { allowEmpty: true });
return;
}
if (normalized) setMarkedColorPickerHex($row, normalized);
});
$('#marked-inputs-container').off('blur', '.marked-color-input').on('blur', '.marked-color-input', (e) => {
const $row = $(e.currentTarget).closest('.marked-row');
const $popover = $row.find('.marked-color-popover');
const parsedHex = readMarkedColorInputAsHex($popover);
if (parsedHex === '') {
setMarkedColorPickerHex($row, '', { allowEmpty: true });
return;
}
if (parsedHex) {
setMarkedColorPickerHex($row, parsedHex);
return;
}
renderMarkedColorPicker($row);
});
$('#marked-inputs-container').off('click', '.marked-color-format').on('click', '.marked-color-format', (e) => {
e.preventDefault();
e.stopPropagation();
const $row = $(e.currentTarget).closest('.marked-row');
setMarkedColorPickerFormat($row, $(e.currentTarget).data('format'));
return false;
});
$('#marked-inputs-container').off('mousedown touchstart', '.marked-color-sv').on('mousedown touchstart', '.marked-color-sv', (e) => {
e.preventDefault();
e.stopPropagation();
beginMarkedColorDrag($(e.currentTarget).closest('.marked-row'), 'sv', e);
return false;
});
$('#marked-inputs-container').off('mousedown touchstart', '.marked-color-hue').on('mousedown touchstart', '.marked-color-hue', (e) => {
e.preventDefault();
e.stopPropagation();
beginMarkedColorDrag($(e.currentTarget).closest('.marked-row'), 'hue', e);
return false;
});
$('#marked-inputs-container').off('click', '.marked-color-clear').on('click', '.marked-color-clear', (e) => {
e.preventDefault();
e.stopPropagation();
const $row = $(e.currentTarget).closest('.marked-row');
const $popover = $row.find('.marked-color-popover');
$row.find('.marked-color-value').val('');
if ($popover.is(':visible')) {
setMarkedColorPickerHex($row, '', { allowEmpty: true });
const state = $popover.data('pickerState');
if (state) {
state.format = 'hex';
$popover.data('pickerState', state);
renderMarkedColorPicker($row);
}
} else {
updateMarkedRowSwatch($row, '');
}
$popover.hide();
return false;
});
$('#marked-inputs-container').off('click', '.marked-color-save').on('click', '.marked-color-save', (e) => {
e.preventDefault();
e.stopPropagation();
const $row = $(e.currentTarget).closest('.marked-row');
const $popover = $row.find('.marked-color-popover');
const parsedHex = readMarkedColorInputAsHex($popover);
if (parsedHex === null) {
toast('颜色格式无效,应为 #RRGGBB 或 rgb(r, g, b)');
$row.find('.marked-color-input').trigger('focus');
return false;
}
const normalized = spData('normalizeHexColor', parsedHex);
$row.find('.marked-color-value').val(normalized);
if (normalized) {
setMarkedColorPickerHex($row, normalized);
setMarkedColorPickerFormat($row, 'hex');
} else {
setMarkedColorPickerHex($row, '', { allowEmpty: true });
}
updateMarkedRowSwatch($row, normalized);
$popover.hide();
return false;
});
// ========== 导入/导出配置 ==========
function parseJSONC(str) {
// 逐字符解析:只在字符串外部去掉 // 注释和尾随逗号(不清理零宽字符,保留用户数据完整性)
let out = '', inStr = false, esc = false;
for (let i = 0; i < str.length; i++) {
const ch = str[i];
if (inStr) {
out += ch;
if (esc) { esc = false; }
else if (ch === '\\') { esc = true; }
else if (ch === '"') { inStr = false; }
} else {
if (ch === '"') { inStr = true; out += ch; }
else if (ch === '/' && str[i + 1] === '/') {
// 跳过行注释
while (i < str.length && str[i] !== '\n') i++;
out += '\n';
}
else if (ch === ',' && /^\s*[}\]]/.test(str.slice(i + 1))) {
// 尾随逗号:后面紧跟 } 或 ](允许空白),跳过逗号
continue;
}
else { out += ch; }
}
}
return JSON.parse(out);
}
// 固定开启项:UI 中为 disabled + checked,无需导入/导出
const FIXED_KEYS = new Set([
'enableImageHideMode', // 固定启用(applyImageHideMode 仍可导出)
'interceptReplyForm', // 拦截回复中间页
'updateReplyNumbers', // 当页回复编号
'replaceRightSidebar', // 扩展坞增强
'kaomojiEnhancer', // 颜文字拓展(kaomojiSort 仍可导出)
'highlightPO', // 标记Po主
'enhancePostFormLayout', // 发串UI调整
'applyFilters', // 标记/屏蔽-饼干/关键词
'enhanceIsland', // 增强X岛匿名版
'enablePostExpand', // 展开板块页长串
'searchServiceBy4sY', // 野生搜索酱
]);
// 清理字符串值中的零宽/不可见控制字符
function sanitizeValue(val) {
if (typeof val === 'string') {
return val.replace(/[\u200B\u200C\u200D\uFEFF\u200E\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069]/g, '');
}
if (Array.isArray(val)) return val.map(sanitizeValue);
if (val && typeof val === 'object') {
const o = {};
for (const [k2, v2] of Object.entries(val)) o[k2] = sanitizeValue(v2);
return o;
}
return val;
}
// === 使用数据导入导出 ===
const FULL_EXPORT_SCHEMA_VERSION = 1;
const FULL_IMPORT_DIRECT_OVERRIDE_KEYS = [
'enableCookieSwitch', 'enableCookieConfirm', 'disableWatermark', 'enablePaginationDuplication',
'updatePreviewCookie', 'hideEmptyTitleEmail', 'enableExternalImagePreview',
'enableAutoCookieRefresh', 'enableAutoCookieRefreshToast',
'interceptReplyFormUnvcode', 'interceptReplyFormU200B',
'interceptReplyFormAutoCompress', 'enableSeamlessPaging',
'enableAutoSeamlessPaging', 'enableHDImageAndLayoutFix',
'enableLinkBlank', 'enableAutoUrlLinkify', 'enableQuotePreview',
'enableUpdateCheck', 'enableImageContextMenu', 'enableImageHideMode',
'applyImageHideMode', 'enableDraft', 'timeDisplayMode',
'extendQuote', 'kaomojiSort', 'toggleSidebar',
'threadCookieWhitelistDisplayMode', 'poAnnotationSideDisplayMode',
'replyModeDefault', 'replyExtraDefault', 'blockDisplayMode',
'postAfterAction'
];
function mergeFavoriteThreads(localItems, importedItems) {
const local = Array.isArray(localItems) ? spData('normalizeFavoriteThreads', localItems) : [];
const imported = Array.isArray(importedItems) ? spData('normalizeFavoriteThreads', importedItems) : [];
const seen = new Map();
local.forEach((item) => {
const key = String(item.threadId || '').trim();
if (key) seen.set(key, item);
});
imported.forEach((item) => {
const key = String(item.threadId || '').trim();
if (!key) return;
if (!seen.has(key)) {
seen.set(key, item);
} else {
const existing = seen.get(key);
if (!existing.desc && item.desc) existing.desc = item.desc;
}
});
return Array.from(seen.values());
}
function mergeSubscriptionFeeds(localItems, importedItems) {
const local = Array.isArray(localItems) ? localItems : [];
const imported = Array.isArray(importedItems) ? importedItems : [];
const seen = new Map();
local.forEach((item) => {
const key = String(item.uuid || '').trim();
if (key) seen.set(key, item);
});
imported.forEach((item) => {
const key = String(item.uuid || '').trim();
if (!key) return;
if (!seen.has(key)) {
seen.set(key, item);
} else {
const existing = seen.get(key);
if (!existing.desc && item.desc) existing.desc = item.desc;
}
});
return Array.from(seen.values());
}
function mergeMarkedGroups(localGroups, importedGroups) {
const local = Array.isArray(localGroups) ? localGroups : [];
const imported = Array.isArray(importedGroups) ? importedGroups : [];
const result = [];
const usedImported = new Set();
local.forEach((localGroup) => {
const localDesc = localGroup.desc || localGroup.name || '';
let matched = false;
for (let i = 0; i < imported.length; i++) {
if (usedImported.has(i)) continue;
const impGroup = imported[i];
const impDesc = impGroup.desc || impGroup.name || '';
if (localDesc && impDesc && localDesc === impDesc) {
matched = true;
usedImported.add(i);
const localCookies = Array.isArray(localGroup) ? localGroup : (localGroup.cookies || []);
const impCookies = Array.isArray(impGroup) ? impGroup : (impGroup.cookies || []);
const mergedCookies = Array.from(new Set([...localCookies, ...impCookies]));
if (Array.isArray(localGroup) && !localGroup.desc) {
result.push(mergedCookies);
} else {
result.push({ ...localGroup, ...impGroup, desc: impDesc || localDesc, cookies: mergedCookies });
}
break;
}
}
if (!matched) result.push(localGroup);
});
imported.forEach((impGroup, i) => { if (!usedImported.has(i)) result.push(impGroup); });
return result;
}
function mergeBlockedCookies(localGroups, importedGroups) {
return mergeMarkedGroups(localGroups, importedGroups);
}
function mergeWhitelistGroupsForImport(localGroups, importedGroups) {
const combined = [...(Array.isArray(localGroups) ? localGroups : []), ...(Array.isArray(importedGroups) ? importedGroups : [])];
return spData('mergeThreadCookieWhitelistGroups', combined).groups;
}
function mergeBlockedKeywords(localValue, importedValue) {
const localGroups = spData('normalizeBlockedKeywordGroups', localValue);
const importedGroups = spData('normalizeBlockedKeywordGroups', importedValue);
const allLocalKws = new Set(spData('flattenBlockedKeywords', localGroups));
const localRegexKeys = new Set(
localGroups.filter((g) => g.regex).map((g) => String(g.value || ''))
);
const result = localGroups.map((g) => ({ value: g.value, regex: !!g.regex }));
importedGroups.forEach((impGroup) => {
if (impGroup.regex) {
const key = String(impGroup.value || '');
if (key && !localRegexKeys.has(key)) {
result.push({ value: impGroup.value, regex: true });
localRegexKeys.add(key);
}
return;
}
const impKws = Utils.strToList(impGroup.value);
if (!impKws.length) return;
const hasNew = impKws.some((kw) => !allLocalKws.has(kw));
if (hasNew) {
result.push({ value: impGroup.value, regex: false });
impKws.forEach((kw) => allLocalKws.add(kw));
}
});
return result;
}
function mergeSettingsForFullImport(localSettings, importedSettings) {
const result = Object.assign({}, localSettings);
FULL_IMPORT_DIRECT_OVERRIDE_KEYS.forEach((key) => {
if (key in importedSettings) result[key] = importedSettings[key];
});
result.favoriteThreads = mergeFavoriteThreads(localSettings.favoriteThreads, importedSettings.favoriteThreads);
result.subscriptionFeeds = mergeSubscriptionFeeds(localSettings.subscriptionFeeds, importedSettings.subscriptionFeeds);
result.blockedKeywords = mergeBlockedKeywords(localSettings.blockedKeywords, importedSettings.blockedKeywords);
result.markedGroups = mergeMarkedGroups(localSettings.markedGroups, importedSettings.markedGroups);
result.blockedCookies = mergeBlockedCookies(localSettings.blockedCookies, importedSettings.blockedCookies);
result.threadCookieWhitelistGroups = mergeWhitelistGroupsForImport(localSettings.threadCookieWhitelistGroups, importedSettings.threadCookieWhitelistGroups);
return result;
}
function mergeThreadHistoryStore(localStore, importedStore) {
const local = normalizeThreadHistoryStore(localStore);
const imported = normalizeThreadHistoryStore(importedStore);
const result = Object.assign({}, local);
result.items = Object.assign({}, local.items);
Object.keys(imported.items).forEach((key) => {
const impItem = imported.items[key];
if (!result.items[key]) {
result.items[key] = impItem;
} else {
const localItem = result.items[key];
result.items[key] = {
...localItem,
...impItem,
lastVisitedAt: Math.max(Number(localItem.lastVisitedAt) || 0, Number(impItem.lastVisitedAt) || 0),
page: Math.max(Number(localItem.page) || 0, Number(impItem.page) || 0),
visitCount: Math.max(Number(localItem.visitCount) || 0, Number(impItem.visitCount) || 0),
title: impItem.title || localItem.title,
name: impItem.name || localItem.name,
};
}
});
result.order = Object.keys(result.items)
.sort((a, b) => (Number(result.items[b].lastVisitedAt) || 0) - (Number(result.items[a].lastVisitedAt) || 0));
return result;
}
function mergePostHistoryStore(localStore, importedStore) {
const local = normalizePostHistoryStore(localStore);
const imported = normalizePostHistoryStore(importedStore);
const result = Object.assign({}, local);
result.items = Object.assign({}, local.items);
const STATUS_PRIORITY = { confirmed: 3, unconfirmed: 2, pending: 1, failed: 0 };
Object.keys(imported.items).forEach((key) => {
const impItem = imported.items[key];
if (!result.items[key]) {
result.items[key] = impItem;
} else {
const localItem = result.items[key];
const localStatus = STATUS_PRIORITY[localItem.status] || 0;
const impStatus = STATUS_PRIORITY[impItem.status] || 0;
result.items[key] = {
...localItem,
...impItem,
status: impStatus >= localStatus ? impItem.status : localItem.status,
page: Math.max(Number(localItem.page) || 0, Number(impItem.page) || 0),
submittedAt: Math.min(Number(localItem.submittedAt) || Infinity, Number(impItem.submittedAt) || Infinity),
contentText: impItem.contentText || localItem.contentText,
forumName: impItem.forumName || localItem.forumName,
};
}
});
result.order = Object.keys(result.items)
.sort((a, b) => (Number(result.items[b].submittedAt) || 0) - (Number(result.items[a].submittedAt) || 0));
return result;
}
function normalizeDraftExportText(text) {
return String(text || '')
.replace(/^\uFEFF/, '')
.replace(/[\u200B\u200C\u200D\uFEFF\u200E\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069]/g, '')
.trim();
}
function collectDraftsForExport() {
const registry = getDraftRegistry();
const items = {};
registry.forEach((key) => {
const raw = readDraftValue(key);
const clean = normalizeDraftExportText(raw);
if (clean) items[key] = raw;
});
return { registry: Object.keys(items), items };
}
function applyDraftsFromImport(drafts) {
if (!drafts || typeof drafts !== 'object') return { imported: 0, overwritten: 0 };
const items = drafts.items || {};
let imported = 0;
let overwritten = 0;
const newRegistry = new Set(getDraftRegistry());
Object.keys(items).forEach((key) => {
const clean = normalizeDraftExportText(items[key]);
if (!clean) return;
const existing = readDraftValue(key);
if (existing === items[key]) return;
if (existing) overwritten++;
else imported++;
GM_setValue(key, items[key]);
newRegistry.add(key);
});
saveDraftRegistry(Array.from(newRegistry));
return { imported, overwritten };
}
function mergeKaomojiStats(localStats, importedStats) {
const local = (localStats && typeof localStats === 'object') ? localStats : {};
const imported = (importedStats && typeof importedStats === 'object') ? importedStats : {};
const result = Object.assign({}, local);
Object.keys(imported).forEach((key) => {
const impVal = imported[key];
const localVal = result[key];
if (!localVal) {
result[key] = impVal;
} else if (typeof impVal === 'object' && typeof localVal === 'object') {
const localCount = Number(localVal.count) || 0;
const impCount = Number(impVal.count) || 0;
const localLast = Number(localVal.lastUsed) || 0;
const impLast = Number(impVal.lastUsed) || 0;
result[key] = {
count: localCount + impCount,
lastUsed: Math.max(localLast, impLast)
};
}
});
return result;
}
function collectFullExportPayload(selection) {
const payload = {};
if (selection.settings) {
payload.myScriptSettings = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
}
if (selection.threadHistory) {
payload.threadHistory = normalizeThreadHistoryStore(GM_getValue(THREAD_HISTORY_STORAGE_KEY, null));
}
if (selection.postHistory) {
payload.postHistory = normalizePostHistoryStore(GM_getValue(POST_HISTORY_STORAGE_KEY, null));
}
if (selection.drafts) {
payload.drafts = collectDraftsForExport();
}
if (selection.kaomojiStats) {
payload.kaomojiStats = GM_getValue('kaomojiUsageStats', {});
}
if (selection.cookiePrefs) {
payload.cookiePrefs = GM_getValue('xdex_thread_cookie_prefs', {});
}
return payload;
}
function buildFullExportFile(selection) {
const payload = collectFullExportPayload(selection);
const now = new Date();
const timestamp = now.toISOString().replace(/[:]/g, '-').replace(/\..+/, '');
const scriptVersion = typeof VERSION !== 'undefined' ? VERSION : (typeof GM_info !== 'undefined' ? GM_info.script.version : 'unknown');
const platform = typeof GM_info !== 'undefined' && GM_info.scriptHandler ? 'userscript' : 'extension';
return {
file: {
meta: { format: 'xdex-full-export', schemaVersion: FULL_EXPORT_SCHEMA_VERSION, exportedAt: now.toISOString(), source: 'nmbxd-EX', scriptVersion, platform },
selection,
strategyHints: { settings: 'merge', threadHistory: 'merge', postHistory: 'merge', drafts: 'override-imported', kaomojiStats: 'accumulate', cookiePrefs: 'merge' },
summary: {
threadHistoryCount: selection.threadHistory ? Object.keys((payload.threadHistory || {}).items || {}).length : 0,
postHistoryCount: selection.postHistory ? Object.keys((payload.postHistory || {}).items || {}).length : 0,
draftCount: selection.drafts ? (payload.drafts.registry || []).length : 0,
kaomojiStatsEntries: selection.kaomojiStats ? Object.keys(payload.kaomojiStats || {}).length : 0,
cookiePrefsCount: selection.cookiePrefs ? Object.keys(payload.cookiePrefs || {}).length : 0,
},
payload
},
filename: `x岛-ex-full-export-${timestamp}.json`
};
}
function downloadFullExportFile(fileData) {
const blob = new Blob([JSON.stringify(fileData.file, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = fileData.filename;
a.click();
URL.revokeObjectURL(a.href);
}
function parseFullExportFile(text) {
let parsed;
try { parsed = JSON.parse(text); } catch (e) { return { valid: false, error: '文件格式错误,无法解析 JSON' }; }
if (!parsed || typeof parsed !== 'object') return { valid: false, error: '文件内容无效' };
const meta = parsed.meta || {};
if (meta.format !== 'xdex-full-export') return { valid: false, error: '不支持的文件格式,请使用 X岛-EX 使用数据导出文件' };
if (!meta.schemaVersion || meta.schemaVersion > FULL_EXPORT_SCHEMA_VERSION) return { valid: false, error: `不支持的 schema 版本: ${meta.schemaVersion}` };
if (!parsed.payload || typeof parsed.payload !== 'object') return { valid: false, error: '文件缺少 payload 数据' };
return { valid: true, data: parsed };
}
function applyFullImportPayload(importData) {
const payload = importData.payload;
const report = {};
if (payload.myScriptSettings) {
const local = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
const merged = mergeSettingsForFullImport(local, payload.myScriptSettings);
GM_setValue(SettingPanel.key, merged);
report.settings = { mode: 'merge', changed: true };
}
if (payload.threadHistory) {
const local = normalizeThreadHistoryStore(GM_getValue(THREAD_HISTORY_STORAGE_KEY, null));
const merged = mergeThreadHistoryStore(local, payload.threadHistory);
GM_setValue(THREAD_HISTORY_STORAGE_KEY, merged);
report.threadHistory = { mode: 'merge', count: Object.keys(merged.items || {}).length };
}
if (payload.postHistory) {
const local = normalizePostHistoryStore(GM_getValue(POST_HISTORY_STORAGE_KEY, null));
const merged = mergePostHistoryStore(local, payload.postHistory);
GM_setValue(POST_HISTORY_STORAGE_KEY, merged);
report.postHistory = { mode: 'merge', count: Object.keys(merged.items || {}).length };
}
if (payload.drafts) {
const result = applyDraftsFromImport(payload.drafts);
report.drafts = { mode: 'override-imported', imported: result.imported, overwritten: result.overwritten };
}
if (payload.kaomojiStats) {
const local = GM_getValue('kaomojiUsageStats', {});
const merged = mergeKaomojiStats(local, payload.kaomojiStats);
GM_setValue('kaomojiUsageStats', merged);
report.kaomojiStats = { mode: 'accumulate', changed: true };
}
if (payload.cookiePrefs) {
const local = GM_getValue('xdex_thread_cookie_prefs', {});
const merged = Object.assign({}, local, payload.cookiePrefs);
GM_setValue('xdex_thread_cookie_prefs', merged);
report.cookiePrefs = { mode: 'merge', count: Object.keys(merged).length };
}
return report;
}
// === 使用数据导入导出 end ===
function buildJSONC(state) {
const filtered = {};
for (const [k, v] of Object.entries(state)) {
if (!FIXED_KEYS.has(k)) filtered[k] = v; // 不清理零宽字符,保留用户原始数据
}
const meta = {
_meta: {
version: (typeof VERSION !== 'undefined' ? VERSION : GM_info.script.version),
exportedAt: new Date().toISOString(),
source: 'nmbxd-EX'
},
settings: filtered
};
const lines = JSON.stringify(meta, null, 2).split('\n');
// 在 _meta 前加注释,在 settings 闭合括号前加尾随逗号
let result = lines.map((line, i) => {
if (i === 0) return line; // 开头 {
if (/"_meta"/.test(line)) return ' // X岛-EX 配置文件\n' + line;
if (/^}$/.test(line.trim())) return line; // 最外层 }
return line;
}).join('\n');
// 给 settings 对象的最后一个字段后加尾随逗号
result = result.replace(/("settings":\s*\{[\s\S]*?)(\n\s*\}\s*\n\s*\})/, (m, inner, close) => {
const trimmed = inner.replace(/,\s*$/, '');
return trimmed + ',\n' + close.replace(/^\n/, '');
});
return result;
}
function validateImport(incoming) {
const defaults = SettingPanel.defaults;
if (typeof incoming !== 'object' || incoming === null) {
toast('配置内容无效'); return null;
}
const validated = {};
let skipped = 0;
for (const [key, val] of Object.entries(incoming)) {
if (FIXED_KEYS.has(key)) continue; // 固定项直接跳过
if (!(key in defaults)) continue; // 未知字段跳过
if (Array.isArray(defaults[key]) && !Array.isArray(val)) { skipped++; continue; }
if (typeof val !== typeof defaults[key]) { skipped++; continue; }
validated[key] = val;
}
return Object.assign({}, defaults, validated);
}
function handleImportText(text) {
if (!text || !text.trim()) { toast('内容为空'); return; }
// 只清理文本开头的 BOM,不清理内容中的零宽字符(用户数据可能包含零宽空格)
text = text.replace(/^\uFEFF/, '');
let parsed;
try {
parsed = parseJSONC(text);
} catch (e) {
toast('配置文件格式错误'); return;
}
const incoming = parsed.settings || parsed;
const merged = validateImport(incoming);
if (!merged) return;
SettingPanel.__pendingImport = merged;
// 显示保存按钮
$('#btn_sp_importExport').removeClass('xdex-inv');
toast('格式正确,请点击[应用]');
}
// 从剪贴板导入
$('#sp_importClipboard').off('click').on('click', async () => {
try {
const text = await navigator.clipboard.readText();
handleImportText(text);
} catch (e) {
toast('无法读取剪贴板,请先点击页面后再试');
}
});
// 导出到剪贴板
$('#sp_exportClipboard').off('click').on('click', async () => {
try {
const jsonc = buildJSONC(SettingPanel.state);
await navigator.clipboard.writeText(jsonc);
toast('已复制到剪贴板');
} catch (e) {
toast('复制失败');
}
});
// 从文件导入
$('#sp_importFile').off('click').on('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,.jsonc,.txt';
input.onchange = () => {
const file = input.files && input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => handleImportText(reader.result);
reader.onerror = () => toast('文件读取失败');
reader.readAsText(file);
};
input.click();
});
// 导出为文件
$('#sp_exportFile').off('click').on('click', () => {
try {
const jsonc = buildJSONC(SettingPanel.state);
const blob = new Blob([jsonc], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'x岛-ex-settings-v' + (typeof VERSION !== 'undefined' ? VERSION : GM_info.script.version) + '.jsonc';
a.click();
URL.revokeObjectURL(a.href);
toast('配置已导出');
} catch (e) {
toast('导出失败');
}
});
// 导入/导出折叠块的"保存"按钮
$('#btn_sp_importExport').off('click').on('click', e => {
e.stopPropagation();
if (SettingPanel.__pendingImport) {
GM_setValue(SettingPanel.key, SettingPanel.__pendingImport);
SettingPanel.state = SettingPanel.__pendingImport;
delete SettingPanel.__pendingImport;
$('#btn_sp_importExport').addClass('xdex-inv');
toast('配置已导入,即将刷新');
setTimeout(() => location.reload(), 800);
} else {
toast('没有待导入的配置');
}
});
// 关闭面板时清除暂存
const _origClose = $('#sp_close,#sp_cover').off.bind($('#sp_close,#sp_cover'), 'click');
delete SettingPanel.__pendingImport;
// ── 使用数据导入导出 ──
$('#btn_sp_fullExport_reset').off('click').on('click', (e) => {
e.stopPropagation();
const selection = {
settings: $('#sp_fullExport_settings').is(':checked'),
threadHistory: $('#sp_fullExport_threadHistory').is(':checked'),
postHistory: $('#sp_fullExport_postHistory').is(':checked'),
drafts: $('#sp_fullExport_drafts').is(':checked'),
kaomojiStats: $('#sp_fullExport_kaomojiStats').is(':checked'),
cookiePrefs: $('#sp_fullExport_cookiePrefs').is(':checked'),
cookiePrefs: $('#sp_fullExport_cookiePrefs').is(':checked'),
};
if (!Object.values(selection).some(Boolean)) { toast('请至少勾选一项'); return; }
const parts = [];
if (selection.settings) parts.push('设置(恢复默认)');
if (selection.threadHistory) parts.push('浏览历史');
if (selection.postHistory) parts.push('发言历史');
if (selection.drafts) parts.push('草稿');
if (selection.kaomojiStats) parts.push('颜文字统计');
if (selection.cookiePrefs) parts.push('串内饼干偏好');
if (!window.confirm(`确定要清除以下项目的全部内容吗?\n\n${parts.join('、')}\n\n清除后页面将自动刷新。`)) return;
try {
if (selection.settings) GM_setValue(SettingPanel.key, {});
if (selection.threadHistory) GM_setValue(THREAD_HISTORY_STORAGE_KEY, normalizeThreadHistoryStore(null));
if (selection.postHistory) GM_setValue(POST_HISTORY_STORAGE_KEY, normalizePostHistoryStore(null));
if (selection.drafts) {
getDraftRegistry().forEach((key) => { try { GM_deleteValue(key); } catch (_) {} });
saveDraftRegistry([]);
}
if (selection.kaomojiStats) GM_setValue('kaomojiUsageStats', {});
if (selection.cookiePrefs) GM_setValue('xdex_thread_cookie_prefs', {});
toast('已清除所选项目,即将刷新');
setTimeout(() => location.reload(), 800);
} catch (err) {
toast('清除失败: ' + (err.message || err));
}
});
$('#btn_sp_fullExport_export').off('click').on('click', (e) => {
e.stopPropagation();
const selection = {
settings: $('#sp_fullExport_settings').is(':checked'),
threadHistory: $('#sp_fullExport_threadHistory').is(':checked'),
postHistory: $('#sp_fullExport_postHistory').is(':checked'),
drafts: $('#sp_fullExport_drafts').is(':checked'),
kaomojiStats: $('#sp_fullExport_kaomojiStats').is(':checked'),
};
if (!Object.values(selection).some(Boolean)) { toast('请至少勾选一项'); return; }
const fileData = buildFullExportFile(selection);
downloadFullExportFile(fileData);
toast('使用数据已导出');
});
$('#btn_sp_fullExport_import').off('click').on('click', (e) => {
e.stopPropagation();
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = () => {
const file = input.files && input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const result = parseFullExportFile(reader.result);
if (!result.valid) { toast(result.error); return; }
SettingPanel.__pendingFullImport = result.data;
renderFullImportPreview(result.data);
};
reader.readAsText(file);
};
input.click();
});
function renderFullImportPreview(data) {
const $preview = $('#sp_fullExport_import_preview').empty().show();
const meta = data.meta || {};
const summary = data.summary || {};
let html = '';
html += `
来源版本: ${meta.scriptVersion || '?'} | 导出时间: ${meta.exportedAt ? new Date(meta.exportedAt).toLocaleString() : '?'}
`;
html += '
包含数据:
';
if (summary.threadHistoryCount) html += `- 浏览历史: ${summary.threadHistoryCount} 条
`;
if (summary.postHistoryCount) html += `- 发言历史: ${summary.postHistoryCount} 条
`;
if (summary.draftCount) html += `- 草稿: ${summary.draftCount} 条
`;
if (summary.kaomojiStatsEntries) html += `- 颜文字统计: ${summary.kaomojiStatsEntries} 项
`;
if (summary.cookiePrefsCount) html += `- 串内饼干偏好: ${summary.cookiePrefsCount} 条
`;
if (data.selection && data.selection.settings) html += '- 设置配置(合并导入)
';
html += '
';
html += '
导入策略: 设置合并、历史合并、草稿冲突时导入端覆盖、颜文字累加
';
html += '
';
const $btn = $('');
$btn.on('click', () => {
if (!SettingPanel.__pendingFullImport) { toast('无可导入数据'); return; }
if (!window.confirm('确定要导入使用数据吗?\n导入完成后页面将自动刷新。')) return;
const report = applyFullImportPayload(SettingPanel.__pendingFullImport);
SettingPanel.__pendingFullImport = null;
$preview.hide();
const parts = [];
if (report.settings) parts.push('设置已合并');
if (report.threadHistory) parts.push(`浏览历史 ${report.threadHistory.count} 条`);
if (report.postHistory) parts.push(`发言历史 ${report.postHistory.count} 条`);
if (report.drafts) parts.push(`草稿 ${report.drafts.imported} 条导入${report.drafts.overwritten ? `, ${report.drafts.overwritten} 条覆盖` : ''}`);
if (report.kaomojiStats) parts.push('颜文字已累加');
toast(`使用数据导入完成: ${parts.join('、')},即将刷新`);
setTimeout(() => location.reload(), 800);
});
$preview.html(html).append($btn);
}
$('#sp_apply').off('click').on('click', ()=>{
collectReloadRequiredSettingsFromPanel();
let valid = true;
const blockedKeywords = collectBlockedKeywordGroupsFromPanel();
if (!blockedKeywords) return;
this.state.blockedKeywords = blockedKeywords;
const favoriteThreads = spUi('collectFavoriteThreadsFromPanel');
if (!favoriteThreads) return;
this.state.favoriteThreads = favoriteThreads;
this.state.replyModeDefault = $('#sp_replyModeDefault').val();
this.state.replyExtraDefault = $('#sp_replyExtraDefault').val();
this.state.kaomojiSort = $('#sp_kaomojiSort').val() || 'default';
this.state.applyImageHideMode = $('#sp_applyImageHideMode').val() || 'default';
this.state.threadCookieWhitelistDisplayMode = $('#sp_threadCookieWhitelistDisplayMode').val() || 'fold';
this.state.poAnnotationSideDisplayMode = $('#sp_poAnnotationSideDisplayMode').val() || 'collapse';
this.state.timeDisplayMode = ($('#sp_timeDisplayMode').val() === 'exact') ? 'exact' : 'relative';
// 标记分组(双字段结构)
const mk = collectMarkedGroupsFromPanel();
if (!mk) return;
this.state.markedGroups = mk;
// 屏蔽分组(双字段结构)
const bk = [];
$('#blocked-inputs-container .blocked-row').each((idx, el)=>{
const $row = $(el);
const desc = ($row.find('.blocked-desc-input').val() || '').trim();
const cookies = Utils.strToList(($row.find('.blocked-cookies-input').val() || '').trim());
if (!desc && !cookies.length) return;
if (!spData('isValidDesc', desc)) { toast(`第${idx + 1}条备注过长`); valid=false; return false; }
if (!cookies.length) { toast(`第${idx + 1}条未指定饼干`); valid=false; return false; }
if (cookies.some(id=>!Utils.cookieLegal(id))) { toast(`第${idx + 1}条存在不合法饼干`); valid=false; return false; }
bk.push({ desc, cookies });
});
if (!valid) return;
this.state.blockedCookies = bk;
const wlg = [];
$('#thread-cookie-whitelist-inputs-container .thread-cookie-whitelist-row').each((idx, el) => {
const $row = $(el);
const desc = ($row.find('.thread-cookie-whitelist-desc-input').val() || '').trim();
const threads = Utils.strToList(($row.find('.thread-cookie-whitelist-threads-input').val() || '').trim());
const cookies = Utils.strToList(($row.find('.thread-cookie-whitelist-cookies-input').val() || '').trim());
if (!desc && !threads.length && !cookies.length) return;
if (!spData('isValidDesc', desc)) { toast(`第${idx + 1}条备注过长`); valid = false; return false; }
if (!threads.length) { toast(`第${idx + 1}条未指定串号`); valid = false; return false; }
if (threads.some(id => !spData('isValidThreadId', id))) { toast(`第${idx + 1}条存在不合法串号`); valid = false; return false; }
if (!cookies.length) { toast(`第${idx + 1}条未指定饼干`); valid = false; return false; }
if (cookies.some(id => !Utils.cookieLegal(id))) { toast(`第${idx + 1}条存在不合法饼干`); valid = false; return false; }
wlg.push({ desc, threads, cookies, rowIndex: idx + 1 });
});
if (!valid) return;
const { groups: mergedWhitelistGroups, mergeEvents } = spData('mergeThreadCookieWhitelistGroups', wlg);
this.state.threadCookieWhitelistGroups = mergedWhitelistGroups;
mergeEvents.forEach((event) => {
if (event.desc) {
toast(`第${event.rowIndex}条(${event.desc})中的串号 ${event.threadId} 已与现有只看规则合并`);
} else {
toast(`第${event.rowIndex}条的串号 ${event.threadId}(${event.cookies.join(',')})已与现有只看规则合并`);
}
});
// 原版
// GM_setValue(this.key, this.state);
// toast('保存成功,即将刷新页面');
// setTimeout(()=>location.reload(),500);
// Edge双重刷新版
// GM_setValue(this.key, this.state);
// console.log('GM_setValue执行成功');
// // 立即读取验证
// const saved = GM_getValue(this.key);
// console.log('读取验证:', JSON.stringify(saved));
// toast('保存成功,即将刷新页面');
// // Edge 浏览器需要更长的延迟确保存储完成
// setTimeout(() => {
// // 再次验证保存是否成功
// const finalCheck = GM_getValue(this.key);
// console.log('刷新前最终验证:', JSON.stringify(finalCheck));
// // 两次刷新
// try {
// // 设置标记,下一次页面加载时脚本会检测到并执行第二次重载
// localStorage.setItem(this.key + '_doSecondReload', '1');
// } catch (e) {
// console.warn('[Settings] set second-reload flag failed', e);
// }
// location.reload();
// }, 800); // 将延迟从 500ms 增加到 800ms
// 当前版本
GM_setValue(this.key, this.state);
console.log('GM_setValue执行成功');
// 立即读取验证
const saved = GM_getValue(this.key);
console.log('读取验证:', JSON.stringify(saved));
toast('保存成功,即将刷新页面');
// Edge 浏览器需要更长的延迟确保存储完成
setTimeout(() => {
// 再次验证保存是否成功
const finalCheck = GM_getValue(this.key);
console.log('刷新前最终验证:', JSON.stringify(finalCheck));
// 只刷新一次
location.reload();
}, 800); // 将延迟从 500ms 增加到 800ms
});
// 关闭面板
$('#sp_close,#sp_cover').off('click').on('click', e=>{
if (e.target.id==='sp_close' || e.target.id==='sp_cover')
$('#sp_cover').fadeOut();
});
//鼠标悬浮在具体功能上显示提示
// ====== 1. 定义功能描述映射表 ======
const spDescriptions = {
sp_enableCookieSwitch: '发帖框上方添加饼干切换器,单击即可快速切换饼干。使用前可单击“刷新”以获取当前登陆账户最新饼干列表。',
sp_enableCookieConfirm: '发送前弹窗显示当前串内各饼干的使用情况,可切换饼干后再发送。',
sp_enablePaginationDuplication: '在串首页添加页码导航栏,并拓展为最长七个页码按钮',
sp_disableWatermark: '取消发图默认勾选的水印选项',
sp_updatePreviewCookie: '为“增强X岛匿名版”添加的预览框显示真实饼干',
sp_hideEmptyTitleEmail: '隐藏帖内无标题、无名氏和版规提示,优化显示效果,减少版面占用',
sp_enableExternalImagePreview: '直接显示外部图床的图片',
sp_enableUpdateCheck: '控制是否自动检查脚本更新;关闭后不会发起远程更新请求,也不会继续安排后续检查。',
sp_enableAutoCookieRefresh: '回到X岛页面后自动刷新饼干,以防错饼',
sp_enableAutoCookieRefreshToast: '自动刷新时显示toast提示,触发频率较高,建议关闭',
sp_enableSeamlessPaging: '阅读到页面底部时无缝加载下一页并为新页首添加页码提示',
sp_enableAutoSeamlessPaging: '滚动到页面底部后自动触发无缝翻页,关闭则可使用按钮手动无缝翻页',
sp_enableHDImageAndLayoutFix: 'X岛-揭示板的增强型体验:默认加载原图而非缩略图,并为所有图片添加X岛自带图片控件;调整布局,防止文字与图片溢出',
sp_enableImageContextMenu: 'userscript模式:为图片/动图启用自定义右键菜单,关闭后保留浏览器原生图片右键菜单,复制图片过程中需要浏览器窗口在前台。\nextension模式:在浏览器右键菜单中添加“X岛-EX:复制GIF/APNG”按钮,仅用于复制GIF/APNG,在复制GIF/APNG过程中可不在前台。',
sp_enableLinkBlank: 'X岛-揭示板的增强型体验:串页链接在新标签页打开',
sp_enableAutoUrlLinkify: '自动将正文中的网址转换为可点击的新标签页蓝色链接,可与“拓展引用格式”共存',
sp_enableQuotePreview: '优化引用弹窗显示,将鼠标悬停出现引用弹窗改为点击显示引用弹窗,引用弹窗可持久存在,支持嵌套、拖拽,点击非引用弹窗区域或ESC键可关闭当前引用弹窗,点击右下角×以关闭全部引用弹窗',
sp_extendQuote: '拓展引用格式,支持除“>>No.66994128”标准引用格式外的引用,例如“>>66994128”、“66994128”、“No.66994128”,同样支持“优化引用弹窗”',
sp_threadCookieWhitelistModeEnabled: '只看饼干模式。\n折叠:保持原版只看饼干折叠逻辑;\n隐藏:未命中的回复直接隐藏;\n分栏:重点回复保留在主阅读流,观众回复进入侧栏批注。\n可选观众回复的展开/收起。',
sp_poAnnotationSideDisplayMode: '分栏模式下观众回复栏的显示状态。展开:完整展开;收起:默认高度不超过对应主回复高度,超出部分滚动。',
sp_toggleSidebar: '来自acVMxuv的自动收起右侧扩展坞侧边栏,鼠标悬停时展开显示',
sp_updateReplyNumbers: '添加当页内回复编号显示',
sp_replaceRightSidebar: '增强右侧扩展坞功能,点击REPLY按钮打开回复弹窗,点击非回复弹窗区域或ESC键可关闭回复弹窗,另外支持使用CTRL+ENTER发送消息',
sp_interceptReplyForm: '拦截回复跳转中间页,使用toast提示发送成功/失败信息',
sp_interceptReplyFormUnvcode: '不可明说的功能,请参照https://words-away.typeboom.com/说明',
sp_interceptReplyFormU200B: '优先使用插入零宽空格模式而非unvcode替换模式',
sp_interceptReplyFormAutoCompress: '自动压缩>2048KB的图片。',
sp_kaomojiEnhancer: '拓展颜文字功能,添加更多颜文字(部分来自蓝岛),优化选择颜文字弹窗,选择颜文字后可插入光标所在处。支持排序:默认(原顺序)/常用(使用次数高优先)/最近(最近使用优先,未使用保持默认顺序)。',
sp_highlightPO: '为回复添加Po主标志,PO主回复编号使用角标显示',
sp_enhancePostFormLayout: '优化发串/回复表单布局,将“送出”按钮移至颜文字栏目,折叠“标题”“E-mail”“名称”等不常用项目,节省版面,添加标题/作者/E-mail/正文的字数统计与提醒',
sp_applyFilters: '标记/屏蔽-饼干/关键词过滤规则\n折叠:匹配到的串/回复显示为可展开的按钮\n隐藏:匹配到的串/回复完全隐藏',
sp_enhanceIsland: '增强X岛匿名版:\n1.发串前显示预览:麻麻再也不用担心我的ASCII ART排版失误了,另外支持预览插入图片和外部图床图片;\n2.自动保存编辑:记忆文本框内容(防止屏蔽词导致被吞),可以在翻页等各种页面切换后保存,仅在“回复成功”后删除,按主串号 "/t/xxxx" 分开存储;\n3.追记引用串号:点击串号回复时附加到光标所在处(或替换文本选区),可追记多条引用;\n4.人类友好的时间显示:如“5秒前”、“1小时前”、“昨天”等;\n5.粘贴插入图片:直接粘贴,将自动作为图片插入\n自动添加标题:将po主设置的标题或者第一行文字 + 页码设置为标签页标题',
sp_timeDisplayMode: '切换串内时间显示方式。相对时间会在当前可见页面定时刷新;精确时间显示原始发帖时间。',
sp_replyQuicklyOnBoardPage: '为板块页添加快速回复模式,在板块页即可回串,页面实时更新,无需跳转串内;并额外支持时间线内回串。\n“板块页默认模式”可选“发串/回复”两种模式,“回复默认模式”可选“临时/连续”两种回复模式,临时模式下回复成功即清除回串信息,连续模式可连续回复直到手动清理回串信息,搭配回复浮窗使用效果更佳',
sp_enablePostExpand: '为板块页内串添加“展开/收起”按钮,点击即可切换长串的完整显示与折叠显示',
sp_searchServiceBy4sY: '官方搜索当前不可用,公告详见:https://www.nmbxd1.com/t/56546294\n替换搜索按钮为来自4sYbzEX的“野生搜索酱”,具体使用方法请查阅原串:https://www.nmbxd.com/t/64792841',
sp_enableImageHideMode: '“默认/模糊/无图/Tips”四种模式可选。默认模式不做修改;选择模糊模式时可使用鼠标悬浮暂时预览图片;无图模式隐藏图片;Tips模式随机显示Tips娘,点击后可恢复原图显示',
sp_enableFavoriteThreads: '在侧边栏添加常用串,支持串内一键添加,并优先跳转浏览历史中的最近阅读页',
sp_enableThreadHistory: '保存浏览历史,支持搜索,可切换多种排序方式',
sp_enablePostHistory: '保存发言历史,分为“我的主题/我的回复”,并记录回复所在页面,支持搜索,可切换多种排序方式',
sp_enableSubscriptionFeed: '使用移动端订阅号进行同步,支持添加多个订阅号',
sp_enableImageViewerMode: '阅图模式:以瀑布流方式浏览当前串的所有图片,点击单图进入详情,支持旋转、缩放、键盘翻页(←→方向键切换、[]旋转、+-缩放、0复位、↑↓平移)。右侧“打开”可在设置面板内进入;无图模式下右上角入口会隐藏,可用此按钮作为备选。',
sp_postAfterAction: '发串成功后的行为:新标签页打开新串,或刷新当前板块页回到顶部',
sp_subscriptionFeeds: '管理X岛订阅号,可添加多个订阅号并设置备注,用于在"我的订阅"标签中查看和管理订阅内容',
sp_disableAutoQuote: '在类似https://www.nmbxd1.com/t/67024789?page=23&r=68811442等携带r=参数的串中,保留高亮的同时控制其是否在输入框中自动添加引用号,值班室版块默认不生效',
sp_autoSelectReportReason: '值班室默认选择“其他”理由,且在值班室版块快捷回复面板中默认为“发串”模式以便提交举报',
};
// 更新日志弹窗(放在 spDescriptions 之后,避免引用未定义)
if (!document.getElementById('sp_update_log')) {
const $log = $(
'' +
'
' +
'
' +
'更新日志' +
'✕' +
'
' +
'
' +
(CHANGELOG || '暂无更新说明') +
'
' +
'
' +
'' +
'' +
'' +
'
' +
'
' +
'
'
);
$('body').append($log);
$('#sp_update_log_close,#sp_update_log').on('click', (e) => {
if (e.target.id === 'sp_update_log') {
spUpdate('closeUpdateLogDialog', { treatAsDismiss: false, reason: 'overlay' });
return;
}
if (e.target.id === 'sp_update_log_close') {
const isRemoteMode = $('#sp_update_log').attr('data-update-mode') === 'remote';
spUpdate('closeUpdateLogDialog', { treatAsDismiss: isRemoteMode, reason: 'close-button' });
}
});
$('#sp_update_log_update_now').on('click', () => {
const state = spUpdate('getUpdateCheckState');
spUpdate('closeUpdateLogDialog', { reason: 'update-now' });
spUpdate('flashFooterUpdateHighlight', state.pendingUpdateSource || '');
});
$('#sp_update_log_ignore_version').on('click', () => {
const state = spUpdate('getUpdateCheckState');
if (state.pendingUpdateVersion) {
state.ignoredVersion = state.pendingUpdateVersion;
spUpdate('setUpdateCheckState', state);
spUpdate('updateSettingsButtonBadge', state);
}
spUpdate('closeUpdateLogDialog', { reason: 'ignore-version' });
});
$('#sp_update_log_dismiss_today').on('click', () => {
spUpdate('closeUpdateLogDialog', { treatAsDismiss: true, reason: 'dismiss-today' });
});
}
// 版本号同步到当前脚本版本
try {
const ver = (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) ? GM_info.script.version : '';
if (ver) $('#sp_version_link').text('v' + ver);
} catch (e) {}
$('#sp_version_link').off('click').on('click', (e) => {
e.preventDefault();
spUpdate('openUpdateLogDialog', 'local');
});
// ====== 2. 创建 tooltip 元素并添加样式 ======
if (!$('#sp_tooltip').length) {
$('body').append('');
const tooltipStyle = `
#sp_tooltip {
position: fixed;
max-width: 260px;
background: rgba(0,0,0,0.85);
color: #fff;
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
display: none;
z-index: 100000;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
transition: opacity 0.15s ease;
opacity: 0;
white-space: pre-line;
}
#sp_tooltip.show {
display: block;
opacity: 1;
}
`;
$('