// ==UserScript== // @name X岛-EX // @namespace http://tampermonkey.net/ // @version 2.2.1 // @description X岛-EX 网页端增强,移动端般的浏览体验:快捷切换饼干/ 添加页首页码 / 关闭图片水印 / 预览真实饼干 / 隐藏无标题-无名氏-版规 / 显示外部图床 / 自动刷新饼干 toast提示 / 无缝翻页-自动翻页 / 默认原图+控件 / 新标签打开串 / 优化引用弹窗 / 拓展引用格式 / 当页回复编号 / 扩展坞增强 / 拦截回复中间页 / 颜文字拓展 / 高亮PO主 / 发串UI调整 / 『分组标记饼干』 / 『屏蔽饼干』 / 『只看饼干』 / 『屏蔽关键词』- 隐藏-折叠 / 增强X岛匿名版 / 板块页快速回复 / 展开板块页长串 / 野生搜索酱 / unvcode-零宽空格模式 / 侧边栏收起 / 图片隐藏模式 / 图片自动压缩-非法图像格式(无GCT)GIF重编码 / 链接自动识别 / 设置项导入导出-剪贴板文件 。 // @author XY // @match https://*.nmbxd1.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addValueChangeListener // @grant GM_xmlhttpRequest // @grant GM_deleteValue // @grant GM_listValues // @grant GM_addStyle // @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 // @license WTFPL // @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) // ==/UserScript== /* global $, jQuery */ // @run-at document-end (function($){ 'use strict'; /* -------------------------------------------------- * tag 0. 通用与工具函数 * -------------------------------------------------- */ const VERSION = GM_info.script.version; function cat_version(){ console.log('[version]:', VERSION); } cat_version(); const CHANGELOG = "优化:\n1.屏蔽饼干/关键词:添加折叠/隐藏模式,折叠模式下被匹配项可以点击展开,隐藏模式则直接去除被匹配串/回复\n\n"; const toastQueue = []; let isShowing = false; function toast(msg, duration = 1800) { 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 = $(``); $('body').append($t); $t.fadeIn(240).delay(duration).fadeOut(240, () => { $t.remove(); // ✅ 动画结束后删除节点 showNextToast(); // ✅ 显示下一个 }); } 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,7}$/.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; 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); } }; // 多分组标记时依次使用的背景色(可扩充) const markColors = [ '#66CCFF','#00FFCC','#EE0000','#006666','#0080FF','#FFFF00', '#39C5BB','#9999FF','#FF4004','#3399FF','#D80000','#F6BE71', '#EE82EE','#FFA500','#FFE211','#FAAFBE','#0000FF' ]; // 解析"最后一个冒号分隔"的分组:返回 {desc, list} function parseDescAndListByLastColon(raw) { const idx = Math.max(raw.lastIndexOf(':'), raw.lastIndexOf(':')); let desc = ''; let cookiePart = ''; if (idx > 0) { // 有冒号:冒号前是备注/说明,冒号后是饼干 desc = raw.slice(0, idx).trim(); cookiePart = raw.slice(idx + 1).trim(); } else { // 没有冒号:整个字符串都是饼干 cookiePart = raw.trim(); } const list = Utils.strToList(cookiePart); return { desc, list }; } // 校验分组说明长度(<=20 字符;满足“10个汉字/20个英文字符”的近似约束) function isValidDesc(desc) { return !desc || desc.length <= 20; } function isValidThreadId(threadId) { return /^\d{8}$/.test(threadId); } function parseThreadCookieWhitelistRule(raw) { const idx = Math.max(raw.lastIndexOf(':'), raw.lastIndexOf(':')); let threadPart = ''; let cookiePart = ''; if (idx > 0) { threadPart = raw.slice(0, idx).trim(); cookiePart = raw.slice(idx + 1).trim(); } else { threadPart = raw.trim(); } return { threads: Utils.strToList(threadPart), cookies: Utils.strToList(cookiePart), }; } function buildThreadCookieWhitelistRowHtml(index, group = {}) { const desc = group.desc || ''; const threadText = Array.isArray(group.threads) ? group.threads.join(',') : ''; const cookieText = Array.isArray(group.cookies) ? group.cookies.join(',') : ''; return ` `; } function buildCookieGroupRowHtml(type, index, value, placeholder) { return `
#${index}
`; } function buildCookieGroupTwoFieldRowHtml(type, index, group = {}) { const desc = group.desc || ''; const cookieText = Array.isArray(group.cookies) ? group.cookies.join(',') : ''; return `
#${index}
`; } function normalizeThreadCookieWhitelistGroups(val) { if (!Array.isArray(val)) return []; return val.map((g) => ({ desc: typeof g.desc === 'string' && isValidDesc(g.desc.trim()) ? g.desc.trim() : '', threads: Array.isArray(g.threads) ? [...new Set(g.threads.filter(isValidThreadId))] : [], cookies: Array.isArray(g.cookies) ? [...new Set(g.cookies.filter(Utils.cookieLegal))] : [], })).filter((g) => g.threads.length && g.cookies.length); } function mergeThreadCookieWhitelistGroups(groups) { const threadOrder = []; const threadToState = new Map(); const mergeEvents = []; groups.forEach((group, idx) => { const desc = typeof group.desc === 'string' && isValidDesc(group.desc.trim()) ? group.desc.trim() : ''; const cookies = [...new Set((group.cookies || []).filter(Utils.cookieLegal))]; const threads = [...new Set((group.threads || []).filter(isValidThreadId))]; threads.forEach((threadId) => { if (!threadToState.has(threadId)) { threadToState.set(threadId, { desc, cookies: new Set(), firstIndex: typeof group.rowIndex === 'number' ? group.rowIndex : idx }); threadOrder.push(threadId); } else { mergeEvents.push({ threadId, rowIndex: typeof group.rowIndex === 'number' ? group.rowIndex : idx, desc, cookies, }); } cookies.forEach((cookie) => threadToState.get(threadId).cookies.add(cookie)); }); }); const grouped = new Map(); const mergedGroups = []; threadOrder.forEach((threadId) => { const state = threadToState.get(threadId); const cookies = Array.from(state.cookies); const desc = state.desc || ''; const key = `${desc}\u0002${cookies.slice().sort().join('\u0001')}`; if (!grouped.has(key)) { const group = { desc, threads: [], cookies }; grouped.set(key, group); mergedGroups.push(group); } grouped.get(key).threads.push(threadId); }); return { groups: mergedGroups, mergeEvents, }; } // 兼容旧版本 blockedCookies 值到“组结构” function normalizeBlockedGroups(val) { if (!val) return []; if (typeof val === 'string') { const tokens = Utils.strToList(val); return tokens.map(t=>{ const {desc, list} = parseDescAndListByLastColon(t); const id = list[0] || ''; return id && Utils.cookieLegal(id) ? { desc, cookies:[id] } : null; }).filter(Boolean); } if (Array.isArray(val)) { if (val.length && 'cookies' in val[0]) { return val.map(g=>({ desc: g.desc || '', cookies: Array.isArray(g.cookies) ? g.cookies.filter(Utils.cookieLegal) : [] })).filter(g=>g.cookies.length); } if (val.length && 'cookie' in val[0]) { const map = new Map(); val.forEach(({cookie, desc})=>{ if (!Utils.cookieLegal(cookie)) return; const key = desc || ''; if (!map.has(key)) map.set(key, []); map.get(key).push(cookie); }); return [...map.entries()].map(([desc, cookies])=>({desc, cookies})); } } return []; } function normalizeMarkedGroups(val) { if (!val) return []; if (typeof val === 'string') { const tokens = Utils.strToList(val); return tokens.map(t => { const { desc, list } = parseDescAndListByLastColon(t); const cookies = list.filter(Utils.cookieLegal); return cookies.length ? { desc, cookies } : null; }).filter(Boolean); } if (Array.isArray(val)) { if (val.length && 'cookies' in val[0]) { return val.map(g => ({ desc: typeof g.desc === 'string' && isValidDesc(g.desc.trim()) ? g.desc.trim() : '', cookies: Array.isArray(g.cookies) ? [...new Set(g.cookies.filter(Utils.cookieLegal))] : [] })).filter(g => g.cookies.length); } if (val.length && 'cookie' in val[0]) { const map = new Map(); val.forEach(({ cookie, desc }) => { if (!Utils.cookieLegal(cookie)) return; const key = typeof desc === 'string' && isValidDesc(desc.trim()) ? desc.trim() : ''; if (!map.has(key)) map.set(key, []); map.get(key).push(cookie); }); return [...map.entries()].map(([desc, cookies]) => ({ desc, cookies: [...new Set(cookies)] })); } } return []; } // 双刷新支持:如果上一次保存设置时要求执行第二次重载(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 */ } /* -------------------------------------------------- * tag 1. 设置面板 * -------------------------------------------------- */ const SettingPanel = { key: 'myScriptSettings', defaults: { enableCookieSwitch: true, 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, // 优化引用弹窗 enableImageHideMode: true, // 图片隐藏/无图模式 applyImageHideMode: 'default', // default | blur | noimage | tips enableDraft: true, extendQuote: true, // 拓展引用格式 enablePostExpandAll: true, // 默认展开板块页长串 kaomojiSort: 'default', // 颜文字排序:default | freq | recent toggleSidebar: false, // 侧边栏收起功能 threadCookieWhitelistGroups: [], replyModeDefault: '回复', // 板块页默认模式:发串/回复 replyExtraDefault: '临时', // 板块/时间线默认额外模式:临时/连续 markedGroups: [], blockedCookies: [], blockedKeywords: '', 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 (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 draftBtn = document.getElementById('sp_toggleDraftCache'); if (draftBtn) { const enabled = !!(this.state && this.state.enableDraft); draftBtn.textContent = enabled ? '关闭草稿' : '开启草稿'; draftBtn.setAttribute('aria-pressed', enabled ? 'true' : 'false'); } const expandBtn = document.getElementById('sp_enablePostExpandAll'); if (expandBtn) { const enabled = !!(this.state && this.state.enablePostExpandAll); expandBtn.textContent = enabled ? '全部收起' : '全部展开'; expandBtn.setAttribute('aria-pressed', enabled ? 'true' : 'false'); } }, 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); // 该功能为固定启用项:避免历史配置把它保存为 false 导致下拉无法生效 this.state.enableImageHideMode = true; console.log('init合并后的state:', JSON.stringify(this.state)); // 兼容迁移:屏蔽饼干到组结构 this.state.markedGroups = normalizeMarkedGroups(this.state.markedGroups); this.state.blockedCookies = normalizeBlockedGroups(this.state.blockedCookies); this.state.threadCookieWhitelistGroups = 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 = normalizeMarkedGroups(this.state.markedGroups); this.state.blockedCookies = normalizeBlockedGroups(this.state.blockedCookies); this.state.threadCookieWhitelistGroups = normalizeThreadCookieWhitelistGroups(this.state.threadCookieWhitelistGroups); this.syncInputs(); this.syncAuxiliaryControls(); } }); }, render() { if (!$('#xdex-setting-style').length) { $('head').append(` `); } if (!$('#sp_btn').length) { $('body').append( $('') .on('click',()=>{ if (typeof window.__xdexSyncDarkReaderTheme === 'function') window.__xdexSyncDarkReaderTheme(); $('#sp_cover').fadeIn(); }) ); } const fold = (id,title,ph) => `
${title}
`; 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 = ` `; $('#sp_cover').remove(); $('body').append(html); // 折叠头:统一控制 $('.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 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); // 屏蔽显示模式:即时切换并即时生效(折叠/隐藏) const applyBlockDisplayModeImmediately = () => { const mode = $('#sp_blockDisplayMode').val() || 'fold'; this.state.blockDisplayMode = mode; try { GM_setValue(this.key, this.state); } catch (e) {} if (typeof applyFilters === 'function') { applyFilters(this.state); } }; $('#sp_blockDisplayMode').off('change').on('change', applyBlockDisplayModeImmediately); // 颜文字排序:即时切换并即时生效(无需点“应用更改”) 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); // --- 初始化“全部展开/全部收起”按钮 (id = sp_enablePostExpandAll) --- (function initExpandAllButton() { const btn = document.getElementById('sp_enablePostExpandAll'); if (!btn) return; const updateButton = (state) => { btn.textContent = state ? '全部收起' : '全部展开'; btn.setAttribute('aria-pressed', state ? 'true' : 'false'); }; // 初次渲染时根据 state 设置按钮文字 const initialState = !!(SettingPanel.state && SettingPanel.state.enablePostExpandAll); updateButton(initialState); btn.addEventListener('click', (e) => { e.stopPropagation(); const newState = !SettingPanel.state.enablePostExpandAll; SettingPanel.state.enablePostExpandAll = newState; // 更新按钮文字 btn.textContent = newState ? '全部收起' : '全部展开'; // 找到“当前在看的串” = 视窗中第一个顶部进入视窗的串 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 (newState && !expanded) { toggleBtn.click(); // 全部展开 } else if (!newState && expanded) { toggleBtn.click(); // 全部收起 } }); // 统一补偿滚动,保证锚点位置不变 if (!newState && 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(newState ? '已展开长串' : '已折叠长串'); }); })(); (function initDraftToggleButton() { const btn = document.getElementById('sp_toggleDraftCache'); if (!btn) return; const updateButton = (enabled) => { btn.textContent = enabled ? '关闭草稿' : '开启草稿'; btn.setAttribute('aria-pressed', enabled ? 'true' : 'false'); }; updateButton(!!(SettingPanel.state && SettingPanel.state.enableDraft)); btn.addEventListener('click', (e) => { e.stopPropagation(); const enabled = !SettingPanel.state.enableDraft; SettingPanel.state.enableDraft = enabled; updateButton(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('已开启草稿缓存'); } }); })(); // 标记:新增组输入 $('#btn_group_marked').off('click').on('click', e=>{ e.stopPropagation(); const nextIndex = $('#marked-inputs-container .marked-row').length + 1; $('#marked-inputs-container').append( 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( 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(buildThreadCookieWhitelistRowHtml(nextIndex)); $('#thread-cookie-whitelist-inputs-container .thread-cookie-whitelist-row').last().find('.thread-cookie-whitelist-desc-input').focus(); }); const saveMarkedGroups = ({ fromDelete = false } = {}) => { 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()); if (!desc && !cookies.length) return; if (!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.markedGroups = parsed; GM_setValue(this.key, this.state); this.syncInputs(); toast(fromDelete ? '已删除标记分组' : '标记分组已保存'); applyFilters(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 (!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 ? '已删除屏蔽分组' : '屏蔽分组已保存'); applyFilters(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 (!isValidDesc(desc)) { toast(`第${idx + 1}条备注过长`); valid = false; return false; } if (!threads.length) { toast(`第${idx + 1}条未指定串号`); valid = false; return false; } if (threads.some(id => !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 } = 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 ? '已删除只看饼干分组' : '只看饼干分组已保存'); applyFilters(this.state); 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; }); // 标记:保存 $('#btn_sp_marked').off('click').on('click', e=>{ e.stopPropagation(); saveMarkedGroups(); }); // 屏蔽:保存 $('#btn_sp_blocked').off('click').on('click', e=>{ e.stopPropagation(); saveBlockedGroups(); }); // 屏蔽关键词:单项保存 $('.sp_save').filter('[data-id="sp_blockedKeywords"]').off('click').on('click', e=>{ e.stopPropagation(); const v = $('#sp_blockedKeywords').val().trim(); if (v && !Utils.strToList(v).length) return toast('屏蔽关键词 规则有误'); this.state.blockedKeywords = v; GM_setValue(this.key, this.state); toast('屏蔽关键词已保存'); applyFilters(this.state); }); $('#btn_sp_threadCookieWhitelist').off('click').on('click', e=>{ e.stopPropagation(); saveThreadCookieWhitelistGroups(); }); // ========== 导入/导出配置 ========== 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; } 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; // 应用更改:保存开关、屏蔽(组)、标记(组) $('#sp_apply').off('click').on('click', ()=>{ [ 'enableCookieSwitch', 'disableWatermark', 'enablePaginationDuplication', 'updatePreviewCookie', 'hideEmptyTitleEmail', 'enableExternalImagePreview', 'enableAutoCookieRefresh', 'enableAutoCookieRefreshToast', 'interceptReplyFormUnvcode', 'interceptReplyFormU200B', 'interceptReplyFormAutoCompress', 'enableSeamlessPaging', 'enableAutoSeamlessPaging', 'enableHDImageAndLayoutFix', 'enableLinkBlank', 'enableAutoUrlLinkify', 'enableQuotePreview', 'extendQuote', 'toggleSidebar' ].forEach(k=> this.state[k] = $('#sp_'+k).is(':checked')); // 固定启用:不受面板勾选状态影响 this.state.enableImageHideMode = true; // ====== 新增:sp_enablePostExpandAll 按钮(即时生效 & 持久化) ====== $('#sp_enablePostExpandAll').off('click').on('click', (e)=>{ e.stopPropagation(); // toggle 状态 this.state.enablePostExpandAll = !this.state.enablePostExpandAll; // 更新按钮文字(立即反馈) $('#sp_enablePostExpandAll').text(this.state.enablePostExpandAll ? '全部收起' : '全部展开'); // 立即持久化(无论是否点“应用更改”都生效) try { GM_setValue(this.key, this.state); } catch (err) { console.warn('保存 enablePostExpandAll 失败:', err); } // 立即应用到页面上(如果 enablePostExpand 已经存在则调用 applyPostExpandAllMode) try { if (typeof applyPostExpandAllMode === 'function') { applyPostExpandAllMode(this.state.enablePostExpandAll); } else if (typeof enablePostExpand === 'function') { // 如果 enablePostExpand 尚未初始化,先初始化(enablePostExpand 内也会读取 SettingPanel.state) try { enablePostExpand(); } catch(e){ /* 忽略 */ } // 尝试延迟调用全局应用函数(容错) setTimeout(()=>{ if (typeof applyPostExpandAllMode === 'function') applyPostExpandAllMode(this.state.enablePostExpandAll); }, 80); } } catch (err) { console.warn('applyPostExpandAllMode 调用异常:', err); } }); // 屏蔽关键词 this.state.blockedKeywords = $('#sp_blockedKeywords').val().trim(); 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'; // 标记分组(双字段结构) const mk = []; 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()); if (!desc && !cookies.length) return; if (!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; } mk.push({ desc, cookies }); }); if (!valid) 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 (!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 (!isValidDesc(desc)) { toast(`第${idx + 1}条备注过长`); valid = false; return false; } if (!threads.length) { toast(`第${idx + 1}条未指定串号`); valid = false; return false; } if (threads.some(id => !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 } = 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_enablePaginationDuplication: '在串首页添加页码导航栏', sp_disableWatermark: '取消发图默认勾选的水印选项', sp_updatePreviewCookie: '为“增强X岛匿名版”添加的预览框显示真实饼干', sp_hideEmptyTitleEmail: '隐藏帖内无标题、无名氏和版规提示,优化显示效果,减少版面占用', sp_enableExternalImagePreview: '直接显示外部图床的图片', sp_enableAutoCookieRefresh: '回到X岛页面后自动刷新饼干,以防错饼', sp_enableAutoCookieRefreshToast: '自动刷新时显示toast提示,触发频率较高,建议关闭', sp_enableSeamlessPaging: '阅读到页面底部时无缝加载下一页并为新页首添加页码提示', sp_enableAutoSeamlessPaging: '滚动到页面底部后自动触发无缝翻页,关闭则可使用按钮手动无缝翻页', sp_enableHDImageAndLayoutFix: 'X岛-揭示板的增强型体验:默认加载原图而非缩略图,并为所有图片添加X岛自带图片控件;调整布局,防止文字与图片溢出', sp_enableLinkBlank: 'X岛-揭示板的增强型体验:串页链接在新标签页打开', sp_enableAutoUrlLinkify: '自动将正文中的网址转换为可点击的新标签页蓝色链接,可与“拓展引用格式”共存', sp_enableQuotePreview: '优化引用弹窗显示,将鼠标悬停出现引用弹窗改为点击显示引用弹窗,引用弹窗可持久存在,支持嵌套、拖拽,点击非引用弹窗区域或ESC键可关闭当前引用弹窗,点击右下角×以关闭全部引用弹窗', sp_extendQuote: '拓展引用格式,支持除“>>No.66994128”标准引用格式外的引用,例如“>>66994128”、“66994128”、“No.66994128”,同样支持“优化引用弹窗”', 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”“名称”等不常用项目,节省版面', sp_applyFilters: '标记/屏蔽-饼干/关键词过滤规则\n折叠:匹配到的串/回复显示为可展开的按钮\n隐藏:匹配到的串/回复完全隐藏', sp_enhanceIsland: '增强X岛匿名版:\n1.发串前显示预览:麻麻再也不用担心我的ASCII ART排版失误了,另外支持预览插入图片和外部图床图片;\n2.自动保存编辑:记忆文本框内容(防止屏蔽词导致被吞),可以在翻页等各种页面切换后保存,仅在“回复成功”后删除,按主串号 "/t/xxxx" 分开存储;\n3.追记引用串号:点击串号回复时附加到光标所在处(或替换文本选区),可追记多条引用;\n4.人类友好的时间显示:如“5秒前”、“1小时前”、“昨天”等;\n5.粘贴插入图片:直接粘贴,将自动作为图片插入\n自动添加标题:将po主设置的标题或者第一行文字 + 页码设置为标签页标题', 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娘,点击后可恢复原图显示', }; // 更新日志弹窗(放在 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' || e.target.id === 'sp_update_log_close') { $('#sp_update_log').fadeOut(120); } }); } // 版本号同步到当前脚本版本 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(); $('#sp_update_log').fadeIn(120); }); // ====== 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; } `; $('