Greasy Fork

Greasy Fork is available in English.

X岛-EX

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

当前为 2025-06-27 提交的版本,查看 最新版本

// ==UserScript==
// @name         X岛-EX
// @namespace    http://tampermonkey.net/
// @version      1.2.12
// @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](http://greasyfork.icu/zh-CN/scripts/438164-xd-enhance)
// @note         联动:可使[增强x岛匿名版](http://greasyfork.icu/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'
  ];

  /* --------------------------------------------------
   * 1. 设置面板
   * -------------------------------------------------- */
  const SettingPanel = {
    key: 'myScriptSettings',
    defaults: {
      enableCookieSwitch: true,
      enablePaginationDuplication: true,
      disableWatermark: true,
      updatePreviewCookie: true,
      updateReplyNumbers: true,
      hideEmptyTitleEmail: true,
      markedGroups: [],
      blockedCookies: '',
      blockedKeywords: ''
    },
    state: {},

    init() {
      this.state = Object.assign({}, this.defaults, GM_getValue(this.key, {}));
      this.render();
      GM_addValueChangeListener(this.key,(k,ov,nv,remote)=>{
        if(remote && $('#sp_cover').is(':hidden')){
          this.state = Object.assign({}, this.defaults, nv);
          this.syncInputs();
        }
      });
    },

    render() {
      if (!$('#xdex-setting-style').length) {
        $('head').append('<style id="xdex-setting-style">.xdex-inv{opacity:0;pointer-events:none;}</style>');
      }
      if (!$('#sp_btn').length) {
        $('body').append(
          $('<button id="sp_btn" style="position:fixed;top:10px;right:10px;z-index:10000;'+
            'padding:6px 12px;border:none;background:#2196F3;color:#fff;border-radius:4px;">'+
            'EX设置</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>
      <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>
        ${fold('sp_blockedCookies','屏蔽饼干','3-7 位, 用逗号隔开')}
        ${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_body 和 data-btn 对应按钮
      $('.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%;margin-bottom:6px;padding:5px;"
                  placeholder="3-7 位,用逗号隔开">`
        ).find('input').last().focus();
      });

      // “保存”按钮:收集非空分组并存储
      $('#btn_sp_marked').off('click').on('click', e=>{
        e.stopPropagation();
        const groups = [];
        $('#marked-inputs-container .marked-input').each((_,el)=>{
          const v = $(el).val().trim();
          if (v) groups.push(v);
        });
        this.state.markedGroups = groups;
        GM_setValue(this.key, this.state);
        toast('标记分组已保存');
        applyFilters(this.state);
      });

      // 单项保存(屏蔽)
      $('.sp_save').filter('[data-id^="sp_blocked"]').off('click').on('click', e=>{
        e.stopPropagation();
        const id = $(e.currentTarget).data('id');
        const label = id==='sp_blockedCookies'?'屏蔽饼干':'屏蔽关键词';
        const v = $('#'+id).val().trim();
        if (v && !Utils.strToList(v).length) return toast(`${label} 规则有误`);
        this.state[id.replace('sp_','')] = v;
        GM_setValue(this.key, this.state);
        toast(`${label} 已保存`);
        applyFilters(this.state);
      });

      // 应用更改:保存开关、屏蔽、标记分组
      $('#sp_apply').off('click').on('click', ()=>{
        ['enableCookieSwitch','enablePaginationDuplication','disableWatermark',
         'updatePreviewCookie','updateReplyNumbers','hideEmptyTitleEmail']
        .forEach(k=> this.state[k] = $('#sp_'+k).is(':checked'));
        this.state.blockedCookies  = $('#sp_blockedCookies').val().trim();
        this.state.blockedKeywords = $('#sp_blockedKeywords').val().trim();
        // 再次收集标记分组
        const groups = [];
        $('#marked-inputs-container .marked-input').each((_,el)=>{
          const v = $(el).val().trim();
          if (v) groups.push(v);
        });
        this.state.markedGroups = groups;
        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']
      .forEach(k=> $('#sp_'+k).prop('checked', this.state[k]));

      // 屏蔽
      $('#sp_blockedCookies').val(this.state.blockedCookies);
      $('#sp_blockedKeywords').val(this.state.blockedKeywords);

      // 标记分组:至少保留一个输入
      const groups = this.state.markedGroups.length ? this.state.markedGroups : [''];
      const $ct = $('#marked-inputs-container').empty();
      groups.forEach(v=>{
        $ct.append(
          `<input class="marked-input" style="width:100%;margin-bottom:6px;padding:5px;"
                  placeholder="3-7 位,用逗号隔开">`
        ).find('input').last().val(v);
      });
      // 初始折叠与按钮隐藏
      $('.sp_fold_body').hide();
      $('#btn_group_marked,#btn_sp_marked').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. 饼干标记 / 屏蔽 逻辑
   * -------------------------------------------------- */
  function markAllCookies(groupStrs) {
    const groups = groupStrs.map(s => Utils.strToList(s));
    $('span.h-threads-info-uid').each(function(){
      const cid = ($(this).text().split(':')[1]||'').trim();
      for(let i=0; i<groups.length; i++){
        if(groups[i].some(p=>Utils.cookieMatch(cid,p))){
          const color = markColors[i % markColors.length];
          $(this).css({ background: color, padding:'0 3px', borderRadius:'2px' });
          break;
        }
      }
    });
  }

  function applyFilters(cfg) {
    markAllCookies(cfg.markedGroups||[]);
    const blkCL = Utils.strToList(cfg.blockedCookies);
    const blkKL = Utils.strToList(cfg.blockedKeywords);
    if(!blkCL.length && !blkKL.length) return;
    const needBlkC = cid=>blkCL.some(p=>Utils.cookieMatch(cid,p));
    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 && needBlkC(cid)){
        const hit = blkCL.find(p=>Utils.cookieMatch(cid,p));
        return Utils.collapse($el, `饼干屏蔽『${hit}』`);
      }
      const kw = Utils.firstHit(txt, blkKL);
      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 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){
    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();
            const desc=tds[3].textContent.trim();
            list[id]={id,name,desc};
          }
        });
        GM_setValue('cookies',list);
        updateDropdownUI(list);
        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 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('饼干信息无效');
      });
    });
    $('#refresh-cookie').on('click',e=>{e.preventDefault();refreshCookies();});
  }

  /* --------------------------------------------------
   * 5. 页面增强:分页复制 / 关闭水印 / 预览区真实饼干 / 隐藏无标题+无名氏
   * -------------------------------------------------- */
  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(); });
  }

  /* --------------------------------------------------
   * 6. 入口初始化
   * -------------------------------------------------- */
  $(document).ready(()=>{
    SettingPanel.init();
    const cfg = GM_getValue(SettingPanel.key, SettingPanel.defaults);

    if(cfg.enableCookieSwitch)          createCookieSwitcherUI();
    if(cfg.enablePaginationDuplication) duplicatePagination();
    if(cfg.disableWatermark)            disableWatermark();
    if(cfg.updatePreviewCookie)         updatePreviewCookieId();
    if(cfg.hideEmptyTitleEmail)         hideEmptyTitleAndEmail();

    if(cfg.updateReplyNumbers)          updateReplyNumbers();
    applyFilters(cfg);
  });

})(jQuery);