您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
X岛揭示板增强:快捷切换饼干/添加页首页码/关闭上传水印/预览真实饼干/当页回复编号/隐藏无标题-无名氏-版规/外部图床显示/自动刷新饼干/『分组标记饼干』/『屏蔽饼干』/『屏蔽关键词』。
// ==UserScript== // @name X岛-EX // @namespace http://tampermonkey.net/ // @version 1.2.15.1 // @description X岛揭示板增强:快捷切换饼干/添加页首页码/关闭上传水印/预览真实饼干/当页回复编号/隐藏无标题-无名氏-版规/外部图床显示/自动刷新饼干/『分组标记饼干』/『屏蔽饼干』/『屏蔽关键词』。 // @author XY // @match https://*.nmbxd1.com/*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addValueChangeListener // @grant GM_xmlhttpRequest // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.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)添加的预览中显示当前饼名(如ID:cOoKiEs),而非ID:cookies // ==/UserScript== (function($){ 'use strict'; /* -------------------------------------------------- * 0. 通用与工具函数 * -------------------------------------------------- */ const toast = msg => { let $t = $('#ae-toast'); if (!$t.length) { $t = $(`<div id="ae-toast" style=" position:fixed;top:10px;left:50%;transform:translateX(-50%); background:rgba(0,0,0,.75);color:#fff;padding:8px 18px; border-radius:5px;z-index:9999;display:none;font-size:14px;"></div>`); $('body').append($t); } $t.text(msg).stop(true).fadeIn(240).delay(1800).fadeOut(240); }; 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 = $(` <div class="xdex-placeholder" style=" padding:6px 10px;background:#fafafa;color:#888; border:1px dashed #bbb;margin-bottom:3px;cursor:pointer;"> ${cap}(点击展开) </div> `); $elem.before($ph).hide().data('xdex-collapsed',true); $ph.on('click',()=>{ if($elem.is(':visible')){ $elem.hide(); $ph.html(`${cap}(点击展开)`); } else { $elem.show(); $ph.text('点击折叠'); } }); } }; // 多分组标记时依次使用的背景色(可扩充) 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(':')); const desc = idx > 0 ? raw.slice(0, idx).trim() : ''; const cookiePart = idx > 0 ? raw.slice(idx + 1).trim() : ''; const list = Utils.strToList(cookiePart); return { desc, list }; } // 校验分组说明长度(<=20 字符;满足“10个汉字/20个英文字符”的近似约束) function isValidDesc(desc) { return !desc || desc.length <= 20; } // 兼容旧版本 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 []; } /* -------------------------------------------------- * 1. 设置面板 * -------------------------------------------------- */ const SettingPanel = { key: 'myScriptSettings', defaults: { enableCookieSwitch: true, enablePaginationDuplication: true, disableWatermark: true, updatePreviewCookie: true, updateReplyNumbers: true, hideEmptyTitleEmail: true, enableExternalImagePreview: true, // 新增:外部图床显示 // 标记与屏蔽都使用组结构:[{desc, cookies:[]}] enableAutoCookieRefresh: true, enableAutoCookieRefreshToast: true, markedGroups: [], blockedCookies: [], blockedKeywords: '' }, state: {}, init() { const saved = GM_getValue(this.key, {}); this.state = Object.assign({}, this.defaults, saved); // 兼容迁移:屏蔽饼干到组结构 this.state.blockedCookies = normalizeBlockedGroups(this.state.blockedCookies); GM_setValue(this.key, this.state); this.render(); GM_addValueChangeListener(this.key,(k,ov,nv,remote)=>{ if(remote && !$('#sp_cover').is(':visible')){ this.state = Object.assign({}, this.defaults, nv); this.state.blockedCookies = normalizeBlockedGroups(this.state.blockedCookies); this.syncInputs(); } }); }, render() { if (!$('#xdex-setting-style').length) { $('head').append(` <style id="xdex-setting-style"> .xdex-inv {opacity:0;pointer-events:none;} /* 默认按钮样式:迷你 EX */ #sp_btn { position:fixed; top:10px; right:10px; z-index:10000; padding:4px 8px; font-size:12px; border:none; background:#2196F3; color:#fff; border-radius:20px; white-space:nowrap; overflow:hidden; max-width: 34px; /* 容纳"EX" */ transition: all 0.3s ease; cursor:pointer; } /* 悬停展开 */ #sp_btn:hover { padding:6px 16px; font-size:14px; max-width:150px; /* 容纳"EX设置" */ } /* 文字渐显 */ #sp_btn span { opacity:0; transition:opacity 0.2s ease; margin-left:4px; } #sp_btn:hover span { opacity:1; } </style> `); } if (!$('#sp_btn').length) { $('body').append( $('<button id="sp_btn">EX<span>设置</span></button>') .on('click',()=>$('#sp_cover').fadeIn()) ); } const fold = (id,title,ph) => ` <div class="sp_fold" style="border:1px solid #eee;margin:6px 0;"> <div class="sp_fold_head" data-btn="#btn_${id}" style="display:flex;align-items:center;padding:6px 8px;background:#fafafa;cursor:pointer;"> <span>${title}</span> <button id="btn_${id}" class="sp_save xdex-inv" data-id="${id}" style="margin-left:auto;padding:2px 8px;">保存</button> </div> <div class="sp_fold_body" style="display:none;padding:8px 10px;"> <input id="${id}" style="width:100%;padding:5px;" placeholder="${ph}"> </div> </div>`; const html = ` <div id="sp_cover" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:9999;"> <div id="sp_panel" style=" position:relative;margin:40px auto;width:480px; max-height:calc(100vh - 80px);background:#fff;border-radius:8px; display:flex;flex-direction:column;box-shadow:0 2px 10px rgba(0,0,0,0.2);"> <div id="sp_panel_content" style="padding:18px;overflow-y:auto;flex:1;min-height:300px;"> <h2 style="margin:0 0 10px;">X岛-EX 设置</h2> <div id="sp_checkbox_container" style="display:flex;flex-wrap:wrap;"> <div style="width:50%;"><input type="checkbox" id="sp_enableCookieSwitch"><label for="sp_enableCookieSwitch"> 快捷切换饼干</label></div> <div style="width:50%;"><input type="checkbox" id="sp_enablePaginationDuplication"><label for="sp_enablePaginationDuplication"> 添加页首页码</label></div> <div style="width:50%;"><input type="checkbox" id="sp_disableWatermark"><label for="sp_disableWatermark"> 关闭上传水印</label></div> <div style="width:50%;"><input type="checkbox" id="sp_updatePreviewCookie"><label for="sp_updatePreviewCookie"> 预览真实饼干</label></div> <div style="width:50%;"><input type="checkbox" id="sp_updateReplyNumbers"><label for="sp_updateReplyNumbers"> 当页回复编号</label></div> <div style="width:50%;"><input type="checkbox" id="sp_hideEmptyTitleEmail"><label for="sp_hideEmptyTitleEmail"> 隐藏无标题/无名氏/版规</label></div> <div style="width:50%;"><input type="checkbox" id="sp_enableExternalImagePreview"><label for="sp_enableExternalImagePreview"> 外部图床显示</label></div> <div style="width:50%;"><input type="checkbox" id="sp_enableAutoCookieRefresh"><label for="sp_enableAutoCookieRefresh"> 饼干自动刷新</label><input type="checkbox" id="sp_enableAutoCookieRefreshToast" style="margin-left:8px;"><label for="sp_enableAutoCookieRefreshToast"> toast提示</label></div> </div> <div style="margin-top:12px;"> <!-- 标记饼干(组) --> <div class="sp_fold"> <div class="sp_fold_head" data-btn="#btn_sp_marked,#btn_group_marked" style="display:flex;align-items:center;padding:6px 8px;background:#fafafa;cursor:pointer;"> <span>标记饼干</span> <button id="btn_group_marked" class="xdex-inv" style="margin-left:auto;padding:2px 8px;">添加分组</button> <button id="btn_sp_marked" class="sp_save xdex-inv" data-id="sp_marked" style="margin-left:4px;padding:2px 8px;">保存</button> </div> <div class="sp_fold_body" style="display:none;padding:8px 10px;"> <div id="marked-inputs-container"></div> </div> </div> <!-- 屏蔽饼干(组,含备注) --> <div class="sp_fold"> <div class="sp_fold_head" data-btn="#btn_sp_blocked,#btn_group_blocked" style="display:flex;align-items:center;padding:6px 8px;background:#fafafa;cursor:pointer;"> <span>屏蔽饼干</span> <button id="btn_group_blocked" class="xdex-inv" style="margin-left:auto;padding:2px 8px;">添加分组</button> <button id="btn_sp_blocked" class="sp_save xdex-inv" data-id="sp_blocked" style="margin-left:4px;padding:2px 8px;">保存</button> </div> <div class="sp_fold_body" style="display:none;padding:8px 10px;"> <div id="blocked-inputs-container"></div> </div> </div> <!-- 屏蔽关键词(单输入) --> ${fold('sp_blockedKeywords','屏蔽关键词','关键词请用逗号隔开,词中包含逗号请加\\\转义')} </div> </div> <div id="sp_panel_footer" style="padding:10px 18px;text-align:right;border-top:1px solid #eee;background:#fff;"> <button id="sp_apply" style="margin-right:10px;padding:6px 10px;">应用更改</button> <button id="sp_close" style="padding:6px 10px;">关闭</button> </div> </div> </div>`; $('#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(); // 标记:新增组输入 $('#btn_group_marked').off('click').on('click', e=>{ e.stopPropagation(); $('#marked-inputs-container').append( `<input class="marked-input" style="width:100%;padding:5px;" placeholder="说明:饼干1,饼干2">` ).find('input').last().focus(); }); // 屏蔽:新增组输入 $('#btn_group_blocked').off('click').on('click', e=>{ e.stopPropagation(); $('#blocked-inputs-container').append( `<input class="blocked-input" style="width:100%;padding:5px;" placeholder="备注:3-7位饼干ID,多个用逗号隔开">` ).find('input').last().focus(); }); // 标记:保存 $('#btn_sp_marked').off('click').on('click', e=>{ e.stopPropagation(); const parsed = []; let valid = true; $('#marked-inputs-container .marked-input').each((_,el)=>{ const v = $(el).val().trim(); if (!v) return; const { desc, list } = parseDescAndListByLastColon(v); if (!list.length) { toast(`“${v}” 未指定饼干`); valid=false; return false; } if (!isValidDesc(desc)) { toast(`“${v}” 分组说明过长`); valid=false; return false; } if (list.some(id=>!Utils.cookieLegal(id))) { toast(`“${v}” 存在不合法饼干`); valid=false; return false; } parsed.push({ desc, cookies: list }); }); if (!valid) return; this.state.markedGroups = parsed; GM_setValue(this.key, this.state); toast('标记分组已保存'); applyFilters(this.state); }); // 屏蔽:保存 $('#btn_sp_blocked').off('click').on('click', e=>{ e.stopPropagation(); const parsed = []; let valid = true; $('#blocked-inputs-container .blocked-input').each((_,el)=>{ const v = $(el).val().trim(); if (!v) return; const { desc, list } = parseDescAndListByLastColon(v); if (!list.length) { toast(`“${v}” 未指定饼干`); valid=false; return false; } if (!isValidDesc(desc)) { toast(`“${v}” 备注过长`); valid=false; return false; } if (list.some(id=>!Utils.cookieLegal(id))) { toast(`“${v}” 存在不合法饼干`); valid=false; return false; } parsed.push({ desc, cookies: list }); }); if (!valid) return; this.state.blockedCookies = parsed; GM_setValue(this.key, this.state); toast('屏蔽饼干已保存'); applyFilters(this.state); }); // 屏蔽关键词:单项保存 $('.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); }); // 应用更改:保存开关、屏蔽(组)、标记(组) $('#sp_apply').off('click').on('click', ()=>{ ['enableCookieSwitch','enablePaginationDuplication','disableWatermark', 'updatePreviewCookie','updateReplyNumbers','hideEmptyTitleEmail','enableExternalImagePreview','enableAutoCookieRefresh','enableAutoCookieRefreshToast'] .forEach(k=> this.state[k] = $('#sp_'+k).is(':checked')); // 屏蔽关键词 this.state.blockedKeywords = $('#sp_blockedKeywords').val().trim(); // 标记分组 const mk = []; let valid = true; $('#marked-inputs-container .marked-input').each((_,el)=>{ const v = $(el).val().trim(); if (!v) return; const { desc, list } = parseDescAndListByLastColon(v); if (!list.length) { toast(`“${v}” 未指定饼干`); valid=false; return false; } if (!isValidDesc(desc)) { toast(`“${v}” 分组说明过长`); valid=false; return false; } if (list.some(id=>!Utils.cookieLegal(id))) { toast(`“${v}” 存在不合法饼干`); valid=false; return false; } mk.push({ desc, cookies: list }); }); if (!valid) return; this.state.markedGroups = mk; // 屏蔽分组 const bk = []; $('#blocked-inputs-container .blocked-input').each((_,el)=>{ const v = $(el).val().trim(); if (!v) return; const { desc, list } = parseDescAndListByLastColon(v); if (!list.length) { toast(`“${v}” 未指定饼干`); valid=false; return false; } if (!isValidDesc(desc)) { toast(`“${v}” 备注过长`); valid=false; return false; } if (list.some(id=>!Utils.cookieLegal(id))) { toast(`“${v}” 存在不合法饼干`); valid=false; return false; } bk.push({ desc, cookies: list }); }); if (!valid) return; this.state.blockedCookies = bk; GM_setValue(this.key, this.state); toast('保存成功,即将刷新页面'); setTimeout(()=>location.reload(),500); }); // 关闭面板 $('#sp_close,#sp_cover').off('click').on('click', e=>{ if (e.target.id==='sp_close' || e.target.id==='sp_cover') $('#sp_cover').fadeOut(); }); }, syncInputs() { // 勾选框 ['enableCookieSwitch','enablePaginationDuplication','disableWatermark', 'updatePreviewCookie','updateReplyNumbers','hideEmptyTitleEmail','enableExternalImagePreview','enableAutoCookieRefresh','enableAutoCookieRefreshToast'] .forEach(k=> $('#sp_'+k).prop('checked', this.state[k])); // 标记分组 const groupsM = this.state.markedGroups.length ? this.state.markedGroups : [{desc:'',cookies:[]}]; const $m = $('#marked-inputs-container').empty(); groupsM.forEach(g=>{ const v = g.desc ? `${g.desc}:${g.cookies.join(',')}` : ''; $m.append( `<input class="marked-input" style="width:100%;padding:5px;" placeholder="说明:饼干1,饼干2">` ).find('input').last().val(v); }); // 屏蔽分组 const groupsB = this.state.blockedCookies.length ? this.state.blockedCookies : [{desc:'',cookies:[]}]; const $b = $('#blocked-inputs-container').empty(); groupsB.forEach(g=>{ const v = g.desc ? `${g.desc}:${g.cookies.join(',')}` : ''; $b.append( `<input class="blocked-input" style="width:100%;padding:5px;" placeholder="备注:3-7位饼干ID,多个用逗号隔开">` ).find('input').last().val(v); }); // 屏蔽关键词 $('#sp_blockedKeywords').val(this.state.blockedKeywords); // 初始折叠与按钮隐藏 $('.sp_fold_body').hide(); $('#btn_group_marked,#btn_sp_marked,#btn_group_blocked,#btn_sp_blocked').addClass('xdex-inv'); } }; /* -------------------------------------------------- * 2. 回复编号 * -------------------------------------------------- */ const circledNumber = n => `『${n}』`; function updateReplyNumbers() { let effectiveCount = 0; $('.h-threads-item-reply-icon').each(function(){ const $reply = $(this).closest('[data-threads-id]'); if ($reply.attr('data-threads-id') === '9999999') { $(this).text(circledNumber(0)); } else { effectiveCount++; $(this).text(circledNumber(effectiveCount)); } }); } /* -------------------------------------------------- * 3. 饼干标记 / 屏蔽 逻辑 * -------------------------------------------------- */ // 标记:支持同一饼干命中多个分组时,title 展示多行备注,颜色取首匹配组 function markAllCookies(groups) { $('span.h-threads-info-uid').each(function(){ const $el = $(this); const cid = ($el.text().split(':')[1]||'').trim(); const hits = []; for (let i=0;i<groups.length;i++){ const g = groups[i]; if (g.cookies.some(p=>Utils.cookieMatch(cid,p))) { if (g.desc) hits.push(g.desc); } } if (!hits.length) return; const firstIdx = groups.findIndex(g=>g.cookies.some(p=>Utils.cookieMatch(cid,p))); const color = markColors[firstIdx % markColors.length]; $el.css({ background: color, padding:'0 3px', borderRadius:'2px' }) .attr('title', hits.join('\n')); }); } function applyFilters(cfg) { // 标记 markAllCookies(cfg.markedGroups||[]); // 屏蔽(按组,匹配到则折叠,文案含备注) const blkG = (cfg.blockedCookies||[]); const blkK = Utils.strToList(cfg.blockedKeywords); const check = $el => { const cid = ($el.find('.h-threads-info-uid').first().text().split(':')[1]||'').trim(); const txt = $el.find('.h-threads-content').first().text(); if (cid && blkG.length) { let matchedCookie = null, matchedDesc = ''; for (let i=0; i<blkG.length && !matchedCookie; i++){ const g = blkG[i]; const hit = g.cookies.find(p=>Utils.cookieMatch(cid,p)); if (hit) { matchedCookie = hit; matchedDesc = g.desc || ''; } } if (matchedCookie) { const label = matchedDesc ? `${matchedCookie}:${matchedDesc}` : matchedCookie; return Utils.collapse($el, `饼干屏蔽『${label}』`); } } const kw = Utils.firstHit(txt, blkK); if (kw) Utils.collapse($el, `关键词屏蔽『${kw}』`); }; if(/\/t\/\d{8,}/.test(location.pathname)){ $('.h-threads-item-reply-main').each((_,el)=>check($(el))); } else { $('.h-threads-item-index').each((_,el)=>{ const $th=$(el); check($th); $th.find('.h-threads-item-reply-main').each((_,s)=>check($(s))); }); } } /* -------------------------------------------------- * 4. 外部图床显示 * -------------------------------------------------- */ const ExternalImagePreview = (function(){ let started = false; const PROCESSED_ATTRIBUTE = 'data-images-processed'; const DEFAULT_VISIBLE = 3; const LOAD_BATCH = 3; const imageUrlRegex = /(https?:\/\/[^\s'")\]}]+?\.(?:jpg|jpeg|png|gif|bmp|webp|svg)(?:\?[^\s'")\]}]*)?)(?=$|\s|['")\]}.,!?])/gi; function buttonCss() { return ` font-size: 12px; padding: 4px 10px; margin-left: 6px; color: #333; background: #fff; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; `; } function createImageItem(url, containerWidth) { const frag = document.createDocumentFragment(); const img = document.createElement('img'); img.src = url; img.style.cssText = ` display: block; margin: 8px auto 2px auto; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; height: auto; `; const linkDiv = document.createElement('div'); linkDiv.style.cssText = ` font-size: 12px; color: #666; margin: 0 auto 10px auto; word-break: break-all; width: fit-content; max-width: 100%; `; const a = document.createElement('a'); a.href = url; a.target = '_blank'; a.textContent = url; a.style.color = '#007bff'; a.addEventListener('click', (e) => e.stopPropagation()); linkDiv.appendChild(a); img.addEventListener('load', () => { const naturalW = img.naturalWidth || 0; if (naturalW > containerWidth) { img.style.width = Math.round(containerWidth * 0.75) + 'px'; img.dataset.state = 'large'; } else { img.style.width = naturalW + 'px'; img.dataset.state = 'small'; } }); img.addEventListener('error', () => { img.style.display = 'none'; linkDiv.style.display = 'none'; }); img.addEventListener('click', (e) => { e.stopPropagation(); const state = img.dataset.state; if (state === 'large') { window.open(url, '_blank'); } else if (state === 'small') { const naturalW = img.naturalWidth || 0; const targetW = Math.round(containerWidth * 0.75); if (!img.dataset.enlarged && targetW > naturalW) { img.style.width = targetW + 'px'; img.dataset.enlarged = 'true'; } else { window.open(url, '_blank'); } } }); frag.appendChild(img); frag.appendChild(linkDiv); return frag; } function appendImages(bodyEl, urls, containerWidth) { const frag = document.createDocumentFragment(); urls.forEach((url) => frag.appendChild(createImageItem(url, containerWidth))); bodyEl.appendChild(frag); } function setCollapsed(container, collapsed) { container.classList.toggle('collapsed', collapsed); container.dataset.collapsed = collapsed ? 'true' : 'false'; container.querySelectorAll('.iic-toggle-btn').forEach((btn) => { btn.textContent = collapsed ? '展开' : '收起'; }); } function injectContainer(afterDiv, imageUrls) { const total = imageUrls.length; if (total === 0) return; const container = document.createElement('div'); container.className = 'injected-image-container'; container.style.cssText = ` margin: 12px 0; border: 1px solid #ddd; border-radius: 6px; background-color: #f9f9f9; box-shadow: 0 1px 2px rgba(0,0,0,0.03); overflow: hidden; `; container.dataset.total = String(total); container.dataset.collapsed = 'false'; const header = document.createElement('div'); header.className = 'iic-header'; header.style.cssText = ` display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; background: #f2f2f2; border-bottom: 1px solid #e6e6e6; cursor: default; user-select: none; `; const title = document.createElement('span'); title.className = 'iic-title'; title.textContent = `图片预览(${total})`; title.style.cssText = `font-size: 13px; color: #333;`; const actions = document.createElement('div'); actions.className = 'iic-actions'; const moreTopBtn = document.createElement('button'); moreTopBtn.className = 'iic-more-btn-top'; moreTopBtn.style.cssText = buttonCss(); const toggleBtn = document.createElement('button'); toggleBtn.className = 'iic-toggle-btn'; toggleBtn.textContent = '收起'; toggleBtn.style.cssText = buttonCss(); toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); const next = container.dataset.collapsed !== 'true'; setCollapsed(container, next); }); actions.appendChild(moreTopBtn); actions.appendChild(toggleBtn); header.appendChild(title); header.appendChild(actions); const body = document.createElement('div'); body.className = 'iic-body'; body.style.cssText = ` padding: 12px 24px 10px 24px; overflow-x: auto; `; const footer = document.createElement('div'); footer.className = 'iic-footer'; footer.style.cssText = ` display: flex; align-items: center; justify-content: center; padding: 8px 12px 12px 12px; background: #f9f9f9; border-top: 1px solid #eee; `; const moreBottomBtn = document.createElement('button'); moreBottomBtn.className = 'iic-more-btn-bottom'; moreBottomBtn.style.cssText = buttonCss(); footer.appendChild(moreBottomBtn); const style = document.createElement('style'); style.textContent = ` .injected-image-container.collapsed .iic-body, .injected-image-container.collapsed .iic-footer { display: none; } `; container.appendChild(style); container.appendChild(header); container.appendChild(body); container.appendChild(footer); afterDiv.parentNode.insertBefore(container, afterDiv.nextSibling); const containerWidth = body.clientWidth || 600; const visibleUrls = imageUrls.slice(0, DEFAULT_VISIBLE); const queue = imageUrls.slice(DEFAULT_VISIBLE); appendImages(body, visibleUrls, containerWidth); function remainingCount() { return queue.length; } function updateMoreButtons() { const rem = remainingCount(); const label = rem > 0 ? `展开更多(剩余${rem},+${LOAD_BATCH})` : '已全部展开'; moreTopBtn.textContent = label; moreBottomBtn.textContent = label; const display = rem > 0 ? '' : 'none'; moreTopBtn.style.display = display; moreBottomBtn.style.display = display; } function loadMore(e) { e.stopPropagation(); if (queue.length === 0) return; const batch = queue.splice(0, LOAD_BATCH); appendImages(body, batch, containerWidth); updateMoreButtons(); } moreTopBtn.addEventListener('click', loadMore); moreBottomBtn.addEventListener('click', loadMore); updateMoreButtons(); container.addEventListener('click', (e) => { const target = e.target; if (target.closest('img, button, a, input, label, textarea, select')) return; if (container.dataset.collapsed !== 'true') setCollapsed(container, true); }); } function processDiv(div) { if (div.hasAttribute(PROCESSED_ATTRIBUTE)) return; div.setAttribute(PROCESSED_ATTRIBUTE, 'true'); const textContent = div.textContent || div.innerText || ''; const matches = textContent.match(imageUrlRegex); if (!matches || matches.length === 0) return; const uniqueImageUrls = [...new Set(matches.map((u) => u.trim()))]; if (uniqueImageUrls.length === 0) return; injectContainer(div, uniqueImageUrls); } function findAndProcessDivs() { const divs = document.querySelectorAll('div.h-threads-content:not([' + PROCESSED_ATTRIBUTE + '])'); divs.forEach(processDiv); } function observeChanges() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { mutation.addedNodes.forEach((node) => { if (node.nodeType !== 1) return; if (node.classList && node.classList.contains('injected-image-container')) return; if (node.classList && node.classList.contains('h-threads-content') && !node.hasAttribute(PROCESSED_ATTRIBUTE)) { processDiv(node); } const childDivs = node.querySelectorAll && node.querySelectorAll('div.h-threads-content:not([' + PROCESSED_ATTRIBUTE + '])'); if (childDivs && childDivs.length > 0) { childDivs.forEach(processDiv); } }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); } function init() { if (started) return; started = true; setTimeout(findAndProcessDivs, 100); observeChanges(); window.addEventListener('load', () => setTimeout(findAndProcessDivs, 500)); // 调试辅助 window.resetImageScript = function resetProcessedElements() { document.querySelectorAll('[' + PROCESSED_ATTRIBUTE + ']').forEach((el) => el.removeAttribute(PROCESSED_ATTRIBUTE)); document.querySelectorAll('.injected-image-container').forEach((c) => c.remove()); }; } return { init }; })(); /* -------------------------------------------------- * 5. 手动切换饼干 + 自动刷新饼干 * -------------------------------------------------- */ const abbreviateName = n => n.replace(/\s*-\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$/, ''); const getCookiesList = () => GM_getValue('cookies', {}); const getCurrentCookie = () => GM_getValue('now-cookie', null); function removeDateString(){ $('#cookie-switcher-ui').find('*').addBack().contents() .filter(function(){ return this.nodeType===3; }) .each(function(){ this.nodeValue = this.nodeValue.replace(/ - 0000-00-00 00:00:00/g,''); }); } function updateCurrentCookieDisplay(cur){ const $d = $('#current-cookie-display'); if(!$d.length) return; if(cur){ const nm = abbreviateName(cur.name); $d.text(nm + (cur.desc ? ' - ' + cur.desc : '')).css('color','#000'); } else { $d.text('已删除').css('color','red'); } removeDateString(); } function updateDropdownUI(list){ const $dd = $('#cookie-dropdown'); $dd.empty(); Object.keys(list).forEach(id=>{ const c=list[id]; const txt=abbreviateName(c.name)+(c.desc?' - '+c.desc:''); $dd.append(`<option value="${id}">${txt}</option>`); }); const cur = getCurrentCookie(); cur && list[cur.id] ? $dd.val(cur.id) : $dd.val(''); removeDateString(); } function switch_cookie(cookie){ if(!cookie || !cookie.id) return toast('无效的饼干信息!'); $.get(`https://www.nmbxd1.com/Member/User/Cookie/switchTo/id/${cookie.id}.html`) .done(()=>{ toast('切换成功! 当前饼干为 '+abbreviateName(cookie.name)); GM_setValue('now-cookie',cookie); updateCurrentCookieDisplay(cookie); updateDropdownUI(getCookiesList()); removeDateString(); updatePreviewCookieId(); }) .fail(()=>toast('切换失败,请重试')); } function refreshCookies(cb, showToast = true){ GM_xmlhttpRequest({ method:'GET', url:'https://www.nmbxd1.com/Member/User/Cookie/index.html', onload:r=>{ if(r.status!==200){ toast('刷新失败 HTTP '+r.status); return cb&&cb(); } const doc=new DOMParser().parseFromString(r.responseText,'text/html'); const rows=doc.querySelectorAll('tbody>tr'), list={}; rows.forEach(row=>{ const tds=row.querySelectorAll('td'); if(tds.length>=4){ const id=tds[1].textContent.trim(); const name=(tds[2].querySelector('a')||{}).textContent?.trim?.() || (tds[2].textContent||'').trim(); const desc=tds[3].textContent.trim(); list[id]={id,name,desc}; } }); GM_setValue('cookies',list); updateDropdownUI(list); if (showToast) { toast('饼干列表已刷新!'); } let cur=getCurrentCookie(); if(cur && !list[cur.id]) cur=null; GM_setValue('now-cookie',cur); updateCurrentCookieDisplay(cur); removeDateString(); updatePreviewCookieId(); cb&&cb(); }, onerror:()=>{ toast('刷新失败,网络错误'); cb&&cb(); } }); } // function autoRefreshCookiesToast() { // try { // if (typeof refreshCookies !== 'function') return; // refreshCookies(() => { // const list = getCookiesList(); // if (!list || !Object.keys(list).length) return; // 刷新失败/未登录时不提示成功 // const cur = getCurrentCookie(); // const nm = cur && cur.name ? abbreviateName(cur.name) : '未知'; // const cfg = SettingPanel.get(); // toast(`自动刷新成功!当前饼干为 ${nm}`); // }); // } catch (e) { // console.warn('自动刷新饼干失败', e); // } //} function showLoginPrompt(){ const $m=$(` <div style="position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:10000;" id="login-modal"> <div style="position:relative;margin:20% auto;width:300px;background:#fff;padding:20px;border-radius:8px;"> <h2>提示</h2><p>当前已退出登录,无法切换饼干。</p> <div style="text-align:right;"> <button id="login-open" style="margin-right:10px;">登录</button> <button id="login-close">关闭</button> </div> </div> </div>`); $('body').append($m); $('#login-open').on('click',()=>{ window.open('https://www.nmbxd1.com/Member/User/Index/login.html','_blank'); $m.fadeOut(200,()=>$m.remove()); }); $('#login-close').on('click',()=>$m.fadeOut(200,()=>$m.remove())); } function createCookieSwitcherUI(){ const $title = $('.h-post-form-title:contains("回应模式")').first(); let $grid = $title.closest('.uk-grid.uk-grid-small.h-post-form-grid'); if(!$grid.length) $grid = $('.h-post-form-title:contains("名 称")').first() .closest('.uk-grid.uk-grid-small.h-post-form-grid'); if(!$grid.length) return; const cur=getCurrentCookie(), list=getCookiesList(); const $ui = $(` <div class="uk-grid uk-grid-small h-post-form-grid" id="cookie-switcher-ui"> <div class="uk-width-1-5"><div class="h-post-form-title">当前饼干</div></div> <div class="uk-width-3-5 h-post-form-input" style="display:flex;align-items:center;justify-content:space-between;"> <div class="uk-flex uk-flex-middle"> <span id="current-cookie-display"></span> <select id="cookie-dropdown" style="margin-left:10px;"></select> </div> <div class="uk-flex uk-flex-right uk-flex-nowrap"> <button id="apply-cookie" class="uk-button uk-button-default" style="margin-right:5px;">应用</button> <button id="refresh-cookie" class="uk-button uk-button-default">刷新</button> </div> </div> </div>`); $grid.before($ui); updateCurrentCookieDisplay(cur); updateDropdownUI(list); $('#apply-cookie').on('click',e=>{ e.preventDefault(); const sel=$('#cookie-dropdown').val(); refreshCookies(()=>{ const l=getCookiesList(); if(!Object.keys(l).length) return showLoginPrompt(); if(!sel) return toast('请选择饼干'); l[sel] ? switch_cookie(l[sel]) : toast('饼干信息无效'); }, false); // ★ 不显示默认toast }); $('#refresh-cookie').on('click', e=>{ e.preventDefault(); refreshCookies(null, true); }); // ★ 显示默认toast } /* -------------------------------------------------- * 6. 页面增强:分页复制 / 关闭水印 / 预览区真实饼干 / 隐藏无标题+无名氏+版规 * -------------------------------------------------- */ function duplicatePagination(){ const tit=document.querySelector('h2.h-title'); const pag=document.querySelector('ul.uk-pagination.uk-pagination-left.h-pagination'); if(!tit||!pag)return; const clone=pag.cloneNode(true); tit.parentNode.insertBefore(clone,tit.nextSibling); clone.querySelectorAll('a').forEach(a=>{ if(a.textContent.trim()==='末页'){ const m=a.href.match(/page=(\d+)/); if(m) a.textContent=`末页(${m[1]})`; } }); } const disableWatermark = () => { const c = document.querySelector('input[type="checkbox"][name="water"][value="true"]'); if(c) c.checked = false; }; function updatePreviewCookieId(){ if(!$('.h-preview-box').length) return; const cur=getCurrentCookie(); const name=cur&&cur.name?abbreviateName(cur.name):'cookies'; $('.h-preview-box .h-threads-info-uid').text('ID:'+name); } function hideEmptyTitleAndEmail(){ $('.h-threads-info-title').each(function(){ if($(this).text().trim()==='无标题') $(this).hide(); }); $('.h-threads-info-email').each(function(){ if($(this).text().trim()==='无名氏') $(this).hide(); }); } /* -------------------------------------------------- * 7. 入口初始化 * -------------------------------------------------- */ $(document).ready(() => { SettingPanel.init(); const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {})); if (cfg.enableCookieSwitch) createCookieSwitcherUI(); if (cfg.enablePaginationDuplication) duplicatePagination(); if (cfg.disableWatermark) disableWatermark(); if (cfg.updatePreviewCookie) updatePreviewCookieId(); if (cfg.hideEmptyTitleEmail) {hideEmptyTitleAndEmail(); Utils.collapse($('.h-forum-header'), '『版规』'); } if (cfg.enableExternalImagePreview) ExternalImagePreview.init(); if (cfg.updateReplyNumbers) updateReplyNumbers(); applyFilters(cfg); // 自动刷新并提示当前饼干 function autoRefreshCookiesToast() { try { if (typeof refreshCookies !== 'function') return; refreshCookies(() => { const list = getCookiesList(); if (!list || !Object.keys(list).length) return; const cur = getCurrentCookie(); const nm = cur && cur.name ? abbreviateName(cur.name) : '未知'; if (cfg.enableAutoCookieRefreshToast) { toast(`自动刷新成功!当前饼干为 ${nm}`); } }, false); // ★ 不显示默认toast } catch (e) { console.warn('自动刷新饼干失败', e); } } // 仅在“从其它标签页切回到本标签页”时刷新 const TAB_ACTIVE_KEY = 'xdex_active_tab'; const tabId = Date.now() + '_' + Math.random().toString(36).slice(2); try { if (document.visibilityState === 'visible') { localStorage.setItem(TAB_ACTIVE_KEY, JSON.stringify({ id: tabId, t: Date.now() })); } } catch {} // ✅ 加上开关判断 if (cfg.enableAutoCookieRefresh) { document.addEventListener('visibilitychange', () => { if (document.visibilityState !== 'visible') return; try { const last = JSON.parse(localStorage.getItem(TAB_ACTIVE_KEY) || 'null'); if (last && last.id && last.id !== tabId) { autoRefreshCookiesToast(); } localStorage.setItem(TAB_ACTIVE_KEY, JSON.stringify({ id: tabId, t: Date.now() })); } catch (e) { try { localStorage.setItem(TAB_ACTIVE_KEY, JSON.stringify({ id: tabId, t: Date.now() })); } catch {} } }, { passive: true }); } }); })(jQuery);