X岛-EX

X岛揭示板增强:快捷切换饼干/添加页首页码/关闭上传水印/预览真实饼干/当页回复编号/隐藏无标题-无名氏-版规/外部图床显示/自动刷新饼干/『分组标记饼干』/『屏蔽饼干』/『屏蔽关键词』。

安装此脚本
作者推荐脚本

您可能也喜欢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);