// ==UserScript==
// @name X岛-EX
// @namespace http://tampermonkey.net/
// @version 2.0.1
// @description X岛-EX 网页端增强,移动端般的浏览体验:快捷切换饼干/ 添加页首页码 / 关闭图片水印 / 预览真实饼干 / 隐藏无标题/无名氏/版规 / 显示外部图床 / 自动刷新饼干 toast提示 / 无缝翻页 自动翻页 / 默认原图+控件 / 新标签打开串 / 优化引用弹窗 / 拓展引用格式 / 当页回复编号 / 扩展坞增强 / 拦截回复中间页 / 颜文字拓展 / 高亮PO主 / 发串UI调整 / 『分组标记饼干』/『屏蔽饼干』/『屏蔽关键词』 / 增强X岛匿名版。
// @author XY
// @match https://*.nmbxd1.com/*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addValueChangeListener
// @grant GM_xmlhttpRequest
// @grant GM_deleteValue
// @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)
// @note 致谢:部分功能移植自[X岛-揭示板的增强型体验](https://greasyfork.org/zh-CN/scripts/497875-x%E5%B2%9B-%E6%8F%AD%E7%A4%BA%E6%9D%BF%E7%9A%84%E5%A2%9E%E5%BC%BA%E5%9E%8B%E4%BD%93%E9%AA%8C#%E8%BF%9E%E6%8E%A5%E7%9B%B4%E6%8E%A5%E8%B7%B3%E8%BD%AC)
// ==/UserScript==
/* global $, jQuery */
// @run-at document-end
(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('点击折叠');
}
});
return $ph;
},
// ===== 引用串优化缓存相关 =====
quoteCache: {},
getQuoteFromCache(id) {
return this.quoteCache[id] || GM_getValue('quote_' + id, null);
},
saveQuoteToCache(id, html) {
this.quoteCache[id] = html;
GM_setValue('quote_' + id, html);
}
};
// 多分组标记时依次使用的背景色(可扩充)
const markColors = [
'#66CCFF','#00FFCC','#EE0000','#006666','#0080FF','#FFFF00',
'#39C5BB','#9999FF','#FF4004','#3399FF','#D80000','#F6BE71',
'#EE82EE','#FFA500','#FFE211','#FAAFBE','#0000FF'
];
// 解析“最后一个冒号分隔”的分组:返回 {desc, list}
function parseDescAndListByLastColon(raw) {
const idx = Math.max(raw.lastIndexOf(':'), raw.lastIndexOf(':'));
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,
duplicatePagination: true,
disableWatermark: true,
enablePaginationDuplication: true,
updatePreviewCookie: true,
hideEmptyTitleEmail: true,
enableExternalImagePreview: true, // 外部图床显示
enableAutoCookieRefresh: true,
enableAutoCookieRefreshToast: false,
enableSeamlessPaging: true,
enableAutoSeamlessPaging: true,
enableHDImage: true, // 启用高清图片链接
enableLinkBlank: true, // 串页新标签打开
enableQuotePreview: true, // 优化引用弹窗
extendQuote: 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 = `
<style>
.fixed-on {
accent-color: #000; /* 勾选颜色黑色 */
}
.fixed-on:disabled {
opacity: 1; /* 不降低透明度 */
filter: none; /* 去掉可能的灰度滤镜 */
cursor: default; /* 鼠标变成普通箭头 */
}
</style>
<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_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 style="width:50%;"><input type="checkbox" id="sp_enableSeamlessPaging"><label for="sp_enableSeamlessPaging"> 无缝翻页</label><input type="checkbox" id="sp_enableAutoSeamlessPaging" checked style="margin-left:8px;"><label for="sp_enableAutoSeamlessPaging"> 自动翻页</label></div>
<div style="width:50%;"><input type="checkbox" id="sp_enableHDImage"><label for="sp_enableHDImage"> 默认原图+控件</label></div>
<div style="width:50%;"><input type="checkbox" id="sp_enableLinkBlank"><label for="sp_enableLinkBlank"> 新标签打开串</label></div>
<div style="width:50%;"><input type="checkbox" id="sp_enableQuotePreview"><label for="sp_enableQuotePreview"> 优化引用弹窗</label></div>
<div style="width:50%;"><input type="checkbox" id="sp_extendQuote"><label for="sp_extendQuote"> 拓展引用格式</label></div>
<!-- 以下是默认勾选项不可更改 -->
<div style="width:50%;"><input type="checkbox" id="sp_updateReplyNumbers" class="fixed-on" checked disabled><label for="sp_updateReplyNumbers"> 当页回复编号</label><input type="hidden" name="sp_updateReplyNumbers" value="1"></div>
<div style="width:50%;"><input type="checkbox" id="sp_replaceRightSidebar" class="fixed-on" checked disabled><label for="sp_replaceRightSidebar"> 扩展坞增强</label><input type="hidden" name="sp_replaceRightSidebar" value="1"></div>
<div style="width:50%;"><input type="checkbox" id="sp_interceptReplyForm" class="fixed-on" checked disabled><label for="sp_interceptReplyForm"> 拦截回复中间页</label><input type="hidden" name="sp_interceptReplyForm" value="1"></div>
<div style="width:50%;"><input type="checkbox" id="sp_kaomojiEnhancer" class="fixed-on" checked disabled><label for="sp_kaomojiEnhancer"> 颜文字拓展</label><input type="hidden" name="sp_kaomojiEnhancer" value="1"></div>
<div style="width:50%;"><input type="checkbox" id="sp_highlightPO" class="fixed-on" checked disabled><label for="sp_highlightPO">标记Po主</label><input type="hidden" name="sp_highlightPO" value="1"></div>
<div style="width:50%;"><input type="checkbox" id="sp_enhancePostFormLayout" class="fixed-on" checked disabled><label for="sp_enhancePostFormLayout"> 发串UI调整</label><input type="hidden" name="sp_enhancePostFormLayout" value="1"></div>
<div style="width:50%;"><input type="checkbox" id="sp_applyFilters" class="fixed-on" checked disabled><label for="sp_applyFilters"> 标记/屏蔽-饼干/关键词</label><input type="hidden" name="sp_applyFilters" value="1"></div>
<div style="width:50%;"><input type="checkbox" id="sp_enhanceIsland" class="fixed-on" checked disabled><label for="sp_enhanceIsland"> 增强X岛匿名版</label><input type="hidden" name="sp_enhanceIsland" value="1"></div>
</div>
<div style="margin-top:12px;">
<!-- 标记饼干(组) -->
<div class="sp_fold" style="border:1px solid #eee;margin:6px 0;">
<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" style="border:1px solid #eee;margin:6px 0;">
<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',
'duplicatePagination',
'disableWatermark',
'enablePaginationDuplication',
'updatePreviewCookie',
'hideEmptyTitleEmail',
'enableExternalImagePreview',
'enableAutoCookieRefresh',
'enableAutoCookieRefreshToast',
'enableSeamlessPaging',
'enableAutoSeamlessPaging',
'enableHDImage',
'enableLinkBlank',
'enableQuotePreview',
'extendQuote'
].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();
});
//鼠标悬浮在具体功能上显示提示
// ====== 1. 定义功能描述映射表 ======
const spDescriptions = {
sp_enableCookieSwitch: '发帖框上方添加饼干切换器,单击即可快速切换饼干。使用前可单击“刷新”以获取当前登陆账户最新饼干列表。',
sp_enablePaginationDuplication: '在串首页添加页码导航栏',
sp_disableWatermark: '取消发图默认勾选的水印选项',
sp_updatePreviewCookie: '为“增强X岛匿名版”添加的预览框显示真实饼干',
sp_hideEmptyTitleEmail: '隐藏帖内无标题、无名氏和版规提示,优化显示效果,减少版面占用',
sp_enableExternalImagePreview: '直接显示外部图床的图片',
sp_enableAutoCookieRefresh: '回到X岛页面后自动刷新饼干,以防错饼',
sp_enableAutoCookieRefreshToast: '自动刷新时显示toast提示,触发频率较高,建议关闭',
sp_enableSeamlessPaging: '阅读到页面底部时无缝加载下一页并为新页首添加页码提示',
sp_enableAutoSeamlessPaging: '滚动到页面底部后自动触发无缝翻页,关闭则可使用按钮手动无缝翻页',
sp_enableHDImage: 'X岛-揭示板的增强型体验:默认加载原图而非缩略图,并为所有图片添加X岛自带图片控件',
sp_enableLinkBlank: 'X岛-揭示板的增强型体验:串页链接在新标签页打开',
sp_enableQuotePreview: '优化引用弹窗显示,将鼠标悬停出现引用弹窗改为点击显示引用弹窗,引用弹窗可持久存在,支持嵌套、拖拽,点击非引用弹窗区域或ESC键可关闭当前引用弹窗,点击右下角×以关闭全部引用弹窗',
sp_extendQuote: '拓展引用格式,支持除“>>No.66994128”标准引用格式外的引用,例如“>>66994128”、“66994128”、“No.66994128”,同样支持“优化引用弹窗”',
sp_updateReplyNumbers: '添加当页内回复编号显示',
sp_replaceRightSidebar: '增强右侧扩展坞功能,点击REPLY按钮打开回复弹窗,点击非回复弹窗区域或ESC键可关闭回复弹窗,另外支持使用CTRL+ENTER发送消息',
sp_interceptReplyForm: '拦截回复跳转中间页,使用toast提示发送成功/失败信息',
sp_kaomojiEnhancer: '拓展颜文字功能,添加更多颜文字(来自蓝岛),优化选择颜文字弹窗,选择颜文字后可插入光标所在处',
sp_highlightPO: '为回复添加Po主标志,PO主回复编号使用角标显示',
sp_enhancePostFormLayout: '优化发串/回复表单布局,将“送出”按钮移至颜文字栏目,折叠“标题”“E-mail”“名称”等不常用项目,节省版面',
sp_applyFilters: '标记/屏蔽-饼干/关键词过滤规则',
sp_enhanceIsland: '增强X岛匿名版:\n1.发串前显示预览:麻麻再也不用担心我的ASCII ART排版失误了,另外支持预览插入图片和外部图床图片;\n2.自动保存编辑:记忆文本框内容(防止屏蔽词导致被吞),可以在翻页等各种页面切换后保存,仅在“回复成功”后删除,按主串号 "/t/xxxx" 分开存储;\n3.追记引用串号:点击串号回复时附加到光标所在处(或替换文本选区),可追记多条引用;\n4.人类友好的时间显示:如“5秒前”、“1小时前”、“昨天”等;\n5.粘贴插入图片:直接粘贴,将自动作为图片插入\n自动添加标题:将po主设置的标题或者第一行文字 + 页码设置为标签页标题'
};
// ====== 2. 创建 tooltip 元素并添加样式 ======
if (!$('#sp_tooltip').length) {
$('body').append('<div id="sp_tooltip"></div>');
const tooltipStyle = `
#sp_tooltip {
position: fixed;
max-width: 260px;
background: rgba(0,0,0,0.85);
color: #fff;
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
display: none;
z-index: 100000;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
transition: opacity 0.15s ease;
opacity: 0;
white-space: pre-line;
}
#sp_tooltip.show {
display: block;
opacity: 1;
}
`;
$('<style>').text(tooltipStyle).appendTo('head');
}
// ====== 3. 绑定事件到每个设置项 ======
$('#sp_checkbox_container input[type=checkbox]').each(function(){
const id = this.id;
const desc = spDescriptions[id];
if (!desc) return;
const $label = $(this).next('label');
const $target = $label.length ? $label : $(this);
$target.on('mouseenter', function(e){
$('#sp_tooltip').text(desc)
.css({ top: e.clientY + 12, left: e.clientX + 12 })
.addClass('show');
}).on('mousemove', function(e){
const offsetX = 40; // 横向偏移量
const offsetY = 0; // 纵向偏移量
$('#sp_tooltip').css({
top: e.clientY + offsetY, // 保持纵向偏移
left: e.clientX + offsetX // 横向偏移改大,右移更多
});
}).on('mouseleave', function(){
$('#sp_tooltip').removeClass('show');
});
});
},
syncInputs() {
// 勾选框
[
'enableCookieSwitch',
'duplicatePagination',
'disableWatermark',
'enablePaginationDuplication',
'updatePreviewCookie',
'hideEmptyTitleEmail',
'enableExternalImagePreview',
'enableAutoCookieRefresh',
'enableAutoCookieRefreshToast',
'enableSeamlessPaging',
'enableAutoSeamlessPaging',
'enableHDImage',
'enableLinkBlank',
'enableQuotePreview',
'extendQuote'
].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. 回复编号
* -------------------------------------------------- */
// 数字样式:包裹为『n』
const circledNumber = n => `『${n}』`;
function updateReplyNumbers() {
// 遍历每一个页面的回复区(含无缝加载的)
$('.h-threads-item-replies').each(function () {
let effectiveCount = 0;
$(this).find('.h-threads-item-reply-icon').each(function () {
const $reply = $(this).closest('[data-threads-id]');
if ($reply.attr('data-threads-id') === '9999999') {
// 特殊:小提示串号 -> 编号 0
$(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;
const $ph = Utils.collapse($el, `饼干屏蔽『${label}』`);
if ($ph && $el.hasClass('h-threads-item-reply-main')) {
// 只标记“被屏蔽的占位符”
$ph.addClass('xdex-placeholder-blocked');
// 仅对这一条回复行启用左右并排(不会影响别处)
const $row = $el.closest('.h-threads-item-reply');
const $icon = $row.find('.h-threads-item-reply-icon').first();
if ($row.length) {
$row.css({ display: 'flex', alignItems: 'flex-start' });
}
if ($icon.length) {
$icon.css({ flex: '0 0 3em', textAlign: 'center' }); // 固定左列宽
}
// 折叠状态(显示“关键词屏蔽…”)应占满;展开状态(“点击折叠”)应缩成小按钮
const applyWidth = () => {
const txt = ($ph.text() || '').trim();
if (txt === '点击折叠') {
$ph.css({
flex: '0 0 auto',
maxWidth: '8em',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
marginRight: '0.5em'
});
} else {
$ph.css({
flex: '0 0 auto',
maxWidth: 'none',
whiteSpace: 'normal',
overflow: 'visible',
textOverflow: 'clip',
marginRight: '0'
});
}
};
// 初次应用一次
applyWidth();
// 点击后(折叠/展开切换文本之后)再应用一次
$ph.off('click.xdex-blocked').on('click.xdex-blocked', () => {
setTimeout(applyWidth, 0);
});
}
return;
}
}
const kw = Utils.firstHit(txt, blkK);
if (kw) {
const $ph = Utils.collapse($el, `关键词屏蔽『${kw}』`);
if ($ph && $el.hasClass('h-threads-item-reply-main')) {
$ph.addClass('xdex-placeholder-blocked');
const $row = $el.closest('.h-threads-item-reply');
const $icon = $row.find('.h-threads-item-reply-icon').first();
if ($row.length) {
$row.css({ display: 'flex', alignItems: 'flex-start' });
}
if ($icon.length) {
$icon.css({ flex: '0 0 3em', textAlign: 'center' });
}
const applyWidth = () => {
const txt = ($ph.text() || '').trim();
if (txt === '点击折叠') {
$ph.css({
flex: '0 0 auto',
maxWidth: '8em',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
marginRight: '0.5em'
});
} else {
$ph.css({
flex: '1 1 auto',
maxWidth: 'none',
whiteSpace: 'normal',
overflow: 'visible',
textOverflow: 'clip',
marginRight: '0'
});
}
};
applyWidth();
$ph.off('click.xdex-blocked').on('click.xdex-blocked', () => {
setTimeout(applyWidth, 0);
});
}
}
};
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;
// 如果是预览框且已经有图片,则直接标记为已处理并跳过
if (div.classList.contains('h-preview-box') && div.querySelector('img')) {
div.setAttribute(PROCESSED_ATTRIBUTE, 'true');
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 + ']), ' +
'div.h-preview-box: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.classList.contains('h-preview-box')) &&
!node.hasAttribute(PROCESSED_ATTRIBUTE)
) {
processDiv(node);
}
const childDivs = node.querySelectorAll && node.querySelectorAll(
'div.h-threads-content:not([' + PROCESSED_ATTRIBUTE + ']), ' +
'div.h-preview-box: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 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" style="display: flex; flex-wrap: nowrap; align-items: center; width: 100%;">
<div class="uk-width-1-5">
<div class="h-post-form-title">饼干</div>
</div>
<div class="uk-width-4-5 h-post-form-input" style="display:flex;align-items:center;gap:8px;flex-wrap:nowrap;">
<div style="flex:1 1 auto;display:flex;align-items:center;gap:6px;min-width:3ch;">
<span id="current-cookie-display" style="max-width:40%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></span>
<select id="cookie-dropdown" style="flex:1 1 auto;min-width:3ch;max-width:100%;"></select>
</div>
<button id="apply-cookie" class="uk-button uk-button-default" style="display:none;">应用</button>
<div style="margin-left:auto;flex:0 0 auto;display:flex;align-items:center;">
<button id="refresh-cookie" class="uk-button uk-button-default" style="min-width:1em;text-align:center;">刷新</button>
</div>
</div>
</div>`);
$grid.before($ui);
updateCurrentCookieDisplay(cur);
updateDropdownUI(list);
// 单击下拉项即切换饼干
$('#cookie-dropdown').on('change', function(){
const sel = $(this).val();
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(null, true);
});
}
/* --------------------------------------------------
* 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(); });
}
function addLastPageNumber(){
document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination a').forEach(a => {
if (a.textContent.trim() === '末页') {
const m = a.href.match(/page=(\d+)/);
if (m && !a.textContent.includes(`(${m[1]})`)) {
a.textContent = `末页(${m[1]})`;
}
}
});
}
// 自动监听 DOM 变化
function observePagination(){
const observer = new MutationObserver(mutations => {
let foundPagination = false;
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue; // 只处理元素节点
// 如果新增的是分页条本身,或它的子元素
if (
node.matches?.('ul.uk-pagination.uk-pagination-left.h-pagination') ||
node.closest?.('ul.uk-pagination.uk-pagination-left.h-pagination')
) {
foundPagination = true;
break;
}
}
if (foundPagination) break; // 已找到就不再继续遍历
}
if (foundPagination) {
addLastPageNumber();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// 初始化
//duplicatePagination();
observePagination();
function initSeamlessPaging() {
try {
const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
if (!cfg.enableSeamlessPaging) return;
let loading = false;
let done = false;
const loadedPages = new Set();
const isThreadPage = /\/t\/\d{4,}/.test(location.pathname);
const isBoardPage = /^\/f\//.test(location.pathname);
const originInfo = (function () {
const cur = new URL(location.href, location.origin);
const threadMatch = location.pathname.match(/\/t\/(\d{4,})/);
return {
origin: location.origin,
threadId: threadMatch ? threadMatch[1] : (document.querySelector('[data-threads-id]')?.getAttribute('data-threads-id') || null),
page: Number(cur.searchParams.get('page') || 1)
};
})();
let lastLoadedPage = originInfo.page || 1;
loadedPages.add(lastLoadedPage);
const SENTINEL_ID = 'hld_auto_page_sentinel';
let sentinel = document.getElementById(SENTINEL_ID);
// 交互状态检测
let hasUserInteracted = false;
let lastUserScrollDir = 0;
let lastScrollTop = window.pageYOffset || document.documentElement.scrollTop || 0;
function onUserScroll() {
hasUserInteracted = true;
const curTop = window.pageYOffset || document.documentElement.scrollTop || 0;
lastUserScrollDir = (curTop > lastScrollTop) ? 1 : (curTop < lastScrollTop ? -1 : lastUserScrollDir);
lastScrollTop = curTop;
}
function onWheel(e) {
hasUserInteracted = true;
if (typeof e.deltaY === 'number') lastUserScrollDir = e.deltaY > 0 ? 1 : -1;
}
window.addEventListener('scroll', onUserScroll, { passive: true });
window.addEventListener('wheel', onWheel, { passive: true });
// ======== 新增:桥接器 + 自动重执行器 ========
function reinitForNewContent(container) {
try {
// 1. 派发自定义事件
document.dispatchEvent(new CustomEvent('SeamlessPageAppended', {
detail: { container }
}));
// 2. 模拟 DOMContentLoaded(部分脚本只监听这个)
document.dispatchEvent(new Event('DOMContentLoaded'));
// 3. 重新执行 container 内的 <script> 标签
container.querySelectorAll('script').forEach(oldScript => {
const newScript = document.createElement('script');
if (oldScript.src) {
newScript.src = oldScript.src;
} else {
newScript.textContent = oldScript.textContent;
}
[...oldScript.attributes].forEach(attr => {
newScript.setAttribute(attr.name, attr.value);
});
oldScript.replaceWith(newScript);
});
} catch (err) {
console.warn('reinitForNewContent error:', err);
}
}
// ============================================
// 串内页容器
function getRootRepliesContainer() {
const root = document.querySelector('.h-threads-item.uk-clearfix[data-threads-id]') ||
document.querySelector('.h-threads-item.uk-clearfix') ||
document.querySelector('[data-threads-id]');
if (!root) return null;
const replies = root.querySelectorAll('.h-threads-item-replies');
if (!replies || replies.length === 0) return null;
return { root, lastReplies: replies[replies.length - 1] };
}
function ensureSentinelPlaced() {
const containers = getRootRepliesContainer();
if (!containers) return;
const { lastReplies } = containers;
if (!sentinel) {
sentinel = document.createElement('div');
sentinel.id = SENTINEL_ID;
sentinel.style.height = '1px';
sentinel.style.width = '100%';
sentinel.style.pointerEvents = 'none';
}
if (lastReplies.nextSibling !== sentinel) {
lastReplies.parentNode.insertBefore(sentinel, lastReplies.nextSibling);
}
}
// 板块页容器
function ensureSentinelPlacedBoard() {
const lists = document.querySelectorAll('.h-threads-list');
const lastList = lists[lists.length - 1];
if (!lastList) return;
if (!sentinel) {
sentinel = document.createElement('div');
sentinel.id = SENTINEL_ID;
sentinel.style.height = '1px';
sentinel.style.width = '100%';
sentinel.style.pointerEvents = 'none';
}
if (lastList.nextSibling !== sentinel) {
lastList.parentNode.insertBefore(sentinel, lastList.nextSibling);
}
}
function removeIdsFromNode(node) {
if (!node || node.querySelectorAll === undefined) return;
node.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
if (node.hasAttribute && node.hasAttribute('id')) node.removeAttribute('id');
}
function parseLastPageFromPagination(pagUl) {
if (!pagUl) return null;
const anchors = Array.from(pagUl.querySelectorAll('a')).map(a => a.href || a.getAttribute('href') || '');
const pageNums = anchors.map(h => {
try {
const u = new URL(h, location.origin);
const p = Number(u.searchParams.get('page') || '') || null;
return p;
} catch (e) {
const m = (h || '').match(/page=(\d+)/);
return m ? Number(m[1]) : null;
}
}).filter(n => !!n);
if (pageNums.length === 0) return null;
return Math.max(...pageNums);
}
function getDomLastPageNum() {
const allPaginations = document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
if (allPaginations.length === 0) return null;
const lastPagination = allPaginations[allPaginations.length - 1];
return parseLastPageFromPagination(lastPagination);
}
function buildThreadPageUrl(threadId, pageNum) {
return `${location.origin}/t/${threadId}?page=${pageNum}`;
}
function computeNextUrl() {
const tid = originInfo.threadId || document.querySelector('[data-threads-id]')?.getAttribute('data-threads-id');
if (!tid) return null;
return buildThreadPageUrl(tid, lastLoadedPage + 1);
}
function extractFromHTML(htmlText) {
const doc = new DOMParser().parseFromString(htmlText, 'text/html');
const repliesAll = doc.querySelectorAll('.h-threads-item-replies');
const replies = repliesAll.length ? repliesAll[0] : doc.querySelector('.h-threads-item-replies');
let pagination = doc.querySelector('ul.uk-pagination.uk-pagination-left.h-pagination') ||
doc.querySelector('ul.uk-pagination.uk-pagination-left') ||
doc.querySelector('ul.uk-pagination');
return { replies, pagination, doc };
}
// 串内页加载
async function loadNext() {
const domLast = getDomLastPageNum();
if (domLast && lastLoadedPage >= domLast) {
done = true;
return;
}
if (loading || done) return;
const nextPageNum = lastLoadedPage + 1;
if (loadedPages.has(nextPageNum)) return;
const nextUrl = computeNextUrl();
if (!nextUrl) { done = true; return; }
loading = true;
try {
const res = await fetch(nextUrl, { credentials: 'same-origin' });
if (!res.ok) { done = true; return; }
const html = await res.text();
const { replies, pagination } = extractFromHTML(html);
if (!replies) { done = true; return; }
let pagClone = pagination ? pagination.cloneNode(true) : null;
if (!pagClone) {
pagClone = document.createElement('ul');
pagClone.className = 'uk-pagination uk-pagination-left h-pagination';
}
pagClone.setAttribute('hld-auto-page', 'ok');
removeIdsFromNode(pagClone);
const lastPageNum = parseLastPageFromPagination(pagClone);
if (lastPageNum) pagClone.setAttribute('data-last-page', String(lastPageNum));
pagClone.setAttribute('data-cloned-page', String(nextPageNum));
const repliesClone = replies.cloneNode(true);
repliesClone.setAttribute('data-cloned-page', String(nextPageNum));
removeIdsFromNode(repliesClone);
const containers = getRootRepliesContainer();
if (!containers) { done = true; return; }
const { lastReplies } = containers;
lastReplies.insertAdjacentElement('afterend', pagClone);
pagClone.insertAdjacentElement('afterend', repliesClone);
// ======== 新增:让其他脚本对新内容生效 ========
reinitForNewContent(repliesClone);
if (cfg.enableHDImage && typeof enableHDImage === 'function') {
enableHDImage(repliesClone);
}
if (cfg.enableLinkBlank && typeof runLinkBlank === 'function') {
runLinkBlank(repliesClone);
}
if (cfg.extendQuote && typeof extendQuote === 'function') {
extendQuote(repliesClone);
}
// 防剧透渲染
if (typeof renderSpoiler === 'function') {
renderSpoiler($(repliesClone));
}
if (typeof initContent === 'function') {
initContent(); // 重新绑定引用悬浮预览
}
initExtendedContent(repliesClone); // 扩展引用
autoHideRefView(repliesClone); // 拓展引用悬浮
// ============================================
// 更新底部分页条
const allPaginations = document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
if (allPaginations.length > 0) {
const lastPagination = allPaginations[allPaginations.length - 1];
if (lastPagination && lastPagination !== pagClone) {
lastPagination.replaceWith(pagClone.cloneNode(true));
}
}
loadedPages.add(nextPageNum);
lastLoadedPage = nextPageNum;
try { history.pushState(null, '', nextUrl); } catch (e) {}
try { if (typeof hideEmptyTitleAndEmail === 'function') hideEmptyTitleAndEmail($(repliesClone)); } catch (e) {}
setTimeout(() => {
try { if (typeof hideEmptyTitleAndEmail === 'function') hideEmptyTitleAndEmail($(repliesClone)); } catch (e) {}
try { if (typeof observePagination === 'function') observePagination(); } catch (e) {}
try { if (typeof updateReplyNumbers === 'function') updateReplyNumbers(); } catch (e) {}
try { if (cfg.enableHDImage && typeof enableHDImage === 'function') enableHDImage(); } catch (e) {}
try { if (cfg.enableLinkBlank && typeof runLinkBlank === 'function') runLinkBlank(); } catch (e) {}
try { if (cfg.extendQuote && typeof extendQuote === 'function') extendQuote(); } catch (e) {}
try { if (cfg.enableQuotePreview && typeof enableQuotePreview === 'function') {
enableQuotePreview();}
} catch (e) {}
try { if (typeof applyFilters === 'function') applyFilters(cfg); } catch (e) {}
try { if (typeof autoHideRefView === 'function') autoHideRefView(); } catch (e) {}
}, 50);
ensureSentinelPlaced();
const hasNextLink = (() => {
const anchorsText = Array.from(pagClone.querySelectorAll('a')).map(a => a.textContent.trim());
if (anchorsText.some(t => /下一页|下页|Next|next|›|»|→/.test(t))) return true;
const parsed = parseLastPageFromPagination(pagClone);
if (parsed && parsed > nextPageNum) return true;
return false;
})();
if (!hasNextLink) done = true;
} catch (e) {
console.warn('seamless paging loadNext error:', e);
done = true;
} finally {
loading = false;
}
}
async function loadNextBoard() {
if (loading || done) return;
const nextPageNum = lastLoadedPage + 1;
if (loadedPages.has(nextPageNum)) return;
const nextUrl = `${location.origin}${location.pathname}?page=${nextPageNum}`;
loading = true;
try {
const res = await fetch(nextUrl, { credentials: 'same-origin' });
if (!res.ok) { done = true; return; }
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const list = doc.querySelector('.h-threads-list');
const pagination = doc.querySelector('ul.uk-pagination.uk-pagination-left.h-pagination');
if (!list) { done = true; return; }
const listClone = list.cloneNode(true);
removeIdsFromNode(listClone);
// 调用隐藏无名氏/无标题 & 回复编号重置
// 插入到 DOM 之后再延迟调用隐藏/编号/过滤
setTimeout(() => {
try { if (typeof hideEmptyTitleAndEmail === 'function') hideEmptyTitleAndEmail(); } catch (e) {}
try { if (typeof updateReplyNumbers === 'function') updateReplyNumbers(); } catch (e) {}
try { if (cfg.enableHDImage && typeof enableHDImage === 'function') enableHDImage(listClone); } catch (e) {}
try { if (cfg.enableLinkBlank && typeof runLinkBlank === 'function') runLinkBlank(listClone); } catch (e) {}
try { if (cfg.extendQuote && typeof extendQuote === 'function') extendQuote(listClone); } catch (e) {}
try { if (cfg.enableQuotePreview && typeof enableQuotePreview === 'function') {
enableQuotePreview();}
} catch (e) {}
try { if (typeof applyFilters === 'function') applyFilters(cfg); } catch (e) {}
try { if (typeof autoHideRefView === 'function') autoHideRefView(listClone); } catch (e) {}
}, 50);
// 找到当前页面最后一个 .h-threads-list
const lists = document.querySelectorAll('.h-threads-list');
const lastList = lists[lists.length - 1];
if (lastList) {
let pagClone = pagination ? pagination.cloneNode(true) : null;
if (pagClone) {
removeIdsFromNode(pagClone);
lastList.insertAdjacentElement('afterend', pagClone);
pagClone.insertAdjacentElement('afterend', listClone);
// ======== 新增:让其他脚本对新内容生效 ========
reinitForNewContent(listClone);
if (cfg.enableHDImage && typeof enableHDImage === 'function') {
enableHDImage(listClone); // ← 新增
}
if (cfg.enableLinkBlank && typeof runLinkBlank === 'function') {
runLinkBlank(listClone); // ← 新增
}
if (cfg.extendQuote && typeof extendQuote === 'function') {
extendQuote(listClone); // ← 新增
}
// 防剧透渲染
if (typeof renderSpoiler === 'function') {
renderSpoiler($(listClone));
}
try { if (cfg.enableQuotePreview && typeof enableQuotePreview === 'function') enableQuotePreview(); } catch (e) {}
if (typeof initContent === 'function') {
initContent(); // 重新绑定引用悬浮预览
}
initExtendedContent(listClone); // 扩展引用
autoHideRefView(listClone); // 拓展引用悬浮
// ============================================
// 更新底部分页条
const allPaginations = document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
if (allPaginations.length > 0) {
const lastPagination = allPaginations[allPaginations.length - 1];
if (lastPagination && lastPagination !== pagClone) {
lastPagination.replaceWith(pagClone.cloneNode(true));
}
}
} else {
lastList.insertAdjacentElement('afterend', listClone);
// ======== 新增:让其他脚本对新内容生效 ========
reinitForNewContent(listClone);
try { if (cfg.enableQuotePreview && typeof enableQuotePreview === 'function') enableQuotePreview(); } catch (e) {}
if (typeof initContent === 'function') {
initContent(); // 重新绑定引用悬浮预览
}
initExtendedContent(listClone); // 扩展引用
autoHideRefView(listClone); // 拓展引用悬浮
// ============================================
}
}
loadedPages.add(nextPageNum);
lastLoadedPage = nextPageNum;
try { history.pushState(null, '', nextUrl); } catch (e) {}
const hasNextLink = pagination && Array.from(pagination.querySelectorAll('a')).some(a => /下一页|下页|Next|›|»|→/i.test(a.textContent));
if (!hasNextLink) done = true;
} catch (e) {
console.warn('board paging loadNext error:', e);
done = true;
} finally {
loading = false;
}
}
function initObserver() {
ensureSentinelPlaced();
if (!sentinel) return;
let observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !loading && !done) {
if (hasUserInteracted && lastUserScrollDir > 0) {
const domLast = getDomLastPageNum();
if (domLast && lastLoadedPage >= domLast) {
done = true;
return;
}
loadNext();
}
}
});
}, { root: null, rootMargin: '0px', threshold: 0.05 });
observer.observe(sentinel);
}
function initManualButton() {
const btn = document.createElement('div');
btn.className = 'xdex-placeholder';
btn.textContent = '加载下一页';
btn.style.cssText = `
padding: 6px 10px;
background: rgb(250, 250, 250);
color: rgb(136, 136, 136);
border: 1px dashed rgb(187, 187, 187);
margin: 10px auto;
cursor: pointer;
width: 100%;
box-sizing: border-box;
text-align: center;
`;
btn.addEventListener('click', () => {
const domLast = getDomLastPageNum();
if (domLast && lastLoadedPage >= domLast) {
done = true;
return;
}
loadNext();
});
ensureSentinelPlaced();
if (sentinel && sentinel.parentNode) {
sentinel.parentNode.insertBefore(btn, sentinel);
}
}
function initObserverBoard() {
ensureSentinelPlacedBoard();
if (!sentinel) return;
let observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !loading && !done) {
if (hasUserInteracted && lastUserScrollDir > 0) {
loadNextBoard();
}
}
});
}, { root: null, rootMargin: '0px', threshold: 0.05 });
observer.observe(sentinel);
}
function initManualButtonBoard() {
const btn = document.createElement('div');
btn.className = 'xdex-placeholder';
btn.textContent = '加载下一页';
btn.style.cssText = `
padding: 6px 10px;
background: rgb(250, 250, 250);
color: rgb(136, 136, 136);
border: 1px dashed rgb(187, 187, 187);
margin: 10px auto;
cursor: pointer;
width: 100%;
box-sizing: border-box;
text-align: center;
`;
btn.addEventListener('click', () => loadNextBoard());
ensureSentinelPlacedBoard();
if (sentinel && sentinel.parentNode) {
sentinel.parentNode.insertBefore(btn, sentinel);
}
}
if (isThreadPage) {
if (cfg.enableAutoSeamlessPaging) {
ensureSentinelPlaced();
initObserver();
} else {
initManualButton();
}
} else if (isBoardPage) {
if (cfg.enableAutoSeamlessPaging) {
ensureSentinelPlacedBoard();
initObserverBoard();
} else {
initManualButtonBoard();
}
}
} catch (err) {
console.error('initSeamlessPaging failed', err);
}
}
/* --------------------------------------------------
* 7. 移植‘X岛-揭示板的增强型体验’功能:启用高清图片链接+图片控件/串在新标签页打开
* -------------------------------------------------- */
function enableHDImage(root = document) {
// 记录每张图片的点击次数
const clickCountMap = new WeakMap();
let lastClickedAnchor = null;
// 1. 把所有 thumb 链接替换成 image
root.querySelectorAll('img').forEach(img => {
if (img.src.includes('/thumb/')) {
img.src = img.src.replace('/thumb/', '/image/');
}
});
root.querySelectorAll('a').forEach(a => {
if (a.href.includes('/thumb/')) {
a.href = a.href.replace('/thumb/', '/image/');
}
});
// 2. 拦截点击事件,按奇偶切换 h-active
root.querySelectorAll('.h-threads-img-a').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
// 如果点击的是另一张图片,重置它的计数
if (lastClickedAnchor !== this) {
lastClickedAnchor = this;
clickCountMap.set(this, 0);
}
// 更新点击次数
let count = (clickCountMap.get(this) || 0) + 1;
clickCountMap.set(this, count);
// 确保图片已替换为高清
const img = this.querySelector('img.h-threads-img');
if (img && img.src.includes('/thumb/')) {
img.src = img.src.replace('/thumb/', '/image/');
}
// 切换 h-active
const box = this.closest('.h-threads-img-box');
if (box) {
if (count % 2 === 1) {
box.classList.add('h-active'); // 奇数次 → 添加
} else {
box.classList.remove('h-active'); // 偶数次 → 移除
}
}
});
});
}
// 页面加载完成后调用
window.addEventListener('load', () => enableHDImage());
function runLinkBlank(root = document) {
root.querySelectorAll('#h-content .h-threads-list a').forEach(a => {
// ===== 新增:排除分页导航内的链接 =====
if (a.closest('ul.uk-pagination.uk-pagination-left.h-pagination')) return;
// =====================================
a.setAttribute('target', '_blank');
});
}
/* --------------------------------------------------
* 8. 回复-发串不跳转中间页/引用优化/创建拓展栏+reply按钮呼出回复悬浮窗
* -------------------------------------------------- */
function enableQuotePreview() {
const cache = Object.create(null);
// 注入样式(只注入一次)
if (!document.getElementById('qp-styles')) {
const style = document.createElement('style');
style.id = 'qp-styles';
style.textContent = `
.qp-overlay-quote {
position: fixed; inset: 0;
z-index: 10500; /* 降低层级 */
background: rgba(0,0,0,.45);
display: none;
}
.qp-stack {
position: fixed;
top: 55%; /* 下移一点 */
left: 50%;
transform: translate(-50%, -50%);
width: min(90vw, 820px);
height: 80vh;
overflow: visible;
box-sizing: border-box;
}
.qp-body .h-post-form-textarea {
width: 75% !important;
min-height: 75px;
box-sizing: border-box;
font-size: 14px;
}
.qp-close-all {
position: fixed; right: 12px; bottom: 12px;
font-size: 20px; line-height: 1;
color: #fff; background: rgba(0,0,0,.6);
padding: 6px 12px; border-radius: 6px; cursor: pointer; z-index: 10000;
user-select: none;
}
.qp-quote {
position: absolute;
top: 0; left: 0;
width: 100%; max-height: 100%;
overflow: auto;
background: #fff;
border: 1px solid #ccc;
outline: 2px solid #fff;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,.24);
padding: 18px 20px 20px;
box-sizing: border-box;
}
.qp-quote * { max-width: 100%; box-sizing: border-box; }
.qp-header {
position: sticky; top: 0;
display: flex; gap: 10px; justify-content: flex-end;
padding-bottom: 6px; margin: -8px -8px 10px; padding: 8px;
background: transparent; /* ✅ 改为透明背景 */
z-index: 2;
}
.qp-level {
font-size: 12px; color: #333; background: #eee; border-radius: 4px; padding: 2px 6px;
}
.qp-back {
font-size: 12px; color: #333; background: #f0f0f0;
border: 1px solid #ccc; border-radius: 4px; padding: 2px 6px;
cursor: pointer;
}
.qp-quote.is-dragging { cursor: grabbing !important; }
#h-ref-view { pointer-events: none !important; }
#h-ref-view {
z-index: 20000 !important; /* 保证原生引用框在浮窗之上 */
}
`;
document.head.appendChild(style);
}
//✅ 保留原生悬浮预览,但让其不拦点击
const observer = new MutationObserver(() => {
const refView = document.getElementById('h-ref-view');
if (refView) {
// 不再隐藏,只在点击/移开时手动关闭
}
});
observer.observe(document.body, { childList: true, subtree: true });
const $overlay = $('<div class="qp-overlay-quote"></div>').appendTo('body');
const $stack = $('<div class="qp-stack"></div>').appendTo($overlay);
const $closeAll= $('<div class="qp-close-all">❌</div>').appendTo($overlay);
$closeAll.on('click', () => {
$stack.empty();
$overlay.fadeOut(160);
});
// 点击引用框以外的任何地方都关闭
$overlay.off('click.qp').on('click.qp', function(e){
if (!$overlay.is(':visible')) return;
const $top = $stack.children('.qp-quote').last();
if (!$top.length) { $overlay.fadeOut(160); return; }
// 1) 如果点击发生在最上层引用框内部,忽略
if ($(e.target).closest($top).length) return;
// 2) 如果正在/刚刚拖拽,避免误触关闭
if ($top.hasClass('is-dragging') || $('.qp-quote.is-dragging').length) return;
// 3) 点击最上层框之外:仅移除最上层
$top.remove();
if ($stack.children('.qp-quote').length === 0) $overlay.fadeOut(160);
});
$(document).on('keydown.qp', e => {
if (e.key !== 'Escape' || !$overlay.is(':visible')) return;
const $last = $stack.children('.qp-quote').last();
if ($last.length) $last.remove();
if ($stack.children().length === 0) $overlay.fadeOut(160);
});
function fetchData(tid) {
if (cache[tid]) return Promise.resolve(cache[tid]);
return $.get(`/Home/Forum/ref?id=${tid}`).then(html => (cache[tid] = html));
}
function stripIds($root) {
$root.find('[id]').removeAttr('id');
return $root;
}
function showQuote(html) {
const depth = $stack.children('.qp-quote').length;
const $quote = $('<div class="qp-quote"></div>').css({
top: '0px',
left: '0px',
zIndex: 1000 + depth
});
const $header = $('<div class="qp-header"></div>');
const $level = $(`<span class="qp-level">第 ${depth + 1} 层</span>`);
const $back = $('<button class="qp-back">返回</button>').on('click', e => {
e.stopPropagation();
$quote.remove();
if ($stack.children().length === 0) $overlay.fadeOut(160);
});
$header.append($level, $back);
$quote.append($header);
const $content = stripIds($('<div></div>').html(html));
$quote.append($content.contents());
enableDragForTop($quote);
$stack.append($quote);
$overlay.fadeIn(160);
enableHDImage($quote[0]);
runLinkBlank($quote[0]);
enableHDImage();
}
function enableDragForTop($quote) {
$stack.children('.qp-quote').off('.qpdrag');
$quote.css({
top: parseInt($quote.css('top')) + 'px',
left: parseInt($quote.css('left')) + 'px'
});
let dragging = false, dx = 0, dy = 0;
$quote.on('mousedown.qpdrag', function(e){
if ($(e.target).closest('a,button,input,textarea,select,label').length) return;
dragging = true;
$quote.addClass('is-dragging');
const qOff = $quote.offset();
dx = e.pageX - qOff.left;
dy = e.pageY - qOff.top;
e.preventDefault();
});
$(document).on('mousemove.qpdrag', function(e){
if (!dragging) return;
const stackOff = $stack.offset();
const top = e.pageY - dy - stackOff.top;
const left = e.pageX - dx - stackOff.left;
$quote.css({ top: Math.max(0, top) + 'px', left: Math.max(0, left) + 'px' });
});
$(document).on('mouseup.qpdrag', function(){
if (!dragging) return;
dragging = false;
$quote.removeClass('is-dragging');
$(document).off('mousemove.qpdrag mouseup.qpdrag');
});
}
$(document).off('click.qp').on('click.qp', 'font[color="#789922"]', function(e){
$('#h-ref-view').hide(); // 点击时关闭原生引用框
e.preventDefault();
e.stopPropagation();
const tid = (this.textContent.match(/\d+/) || [])[0];
if (!tid) return;
fetchData(tid).then(showQuote);
});
$(document).on('mouseleave', 'font[color="#789922"]', function () {
$('#h-ref-view').hide(); // 鼠标移开时关闭原生引用框
});
}
function autoHideRefView() {
setInterval(() => {
const refView = document.getElementById('h-ref-view');
if (!refView || getComputedStyle(refView).display === 'none') return;
// 获取当前引用框中的引用号 ID
const infoId = refView.querySelector('.h-threads-info-id');
if (!infoId) return;
const tidMatch = infoId.textContent.match(/\d+/);
if (!tidMatch) return;
const tid = tidMatch[0];
// 查找所有引用号元素
const quoteFonts = document.querySelectorAll("font[color='#789922']");
let isHovering = false;
quoteFonts.forEach(font => {
const text = font.textContent;
if (text.includes(tid) && font.matches(':hover')) {
isHovering = true;
}
});
// 如果没有任何引用号处于 hover 状态 → 隐藏引用框
if (!isHovering) {
refView.style.display = 'none';
}
}, 300); // 每 300ms 检查一次
}
function bindCtrlEnter(ta) {
if (!ta || ta.__ctrlEnterBound) return;
const form = ta.closest('form');
if (!form) return;
ta.__ctrlEnterBound = true;
// 提交锁
form.addEventListener('submit', function () {
if (form.__submitting) return;
form.__submitting = true;
setTimeout(() => { form.__submitting = false; }, 5000);
});
// Ctrl+Enter 提交
ta.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === 'function') {
e.stopImmediatePropagation();
}
if (form.__submitting) return;
form.__submitting = true;
try { form.requestSubmit(); } catch (_) { form.submit(); }
setTimeout(() => { form.__submitting = false; }, 5000);
}
}, false);
}
function replaceRightSidebar() {
// 移除原始工具栏
$('#h-tool').remove();
// 样式(只注入一次)
if (!document.getElementById('qp-style')) {
const style = document.createElement('style');
style.id = 'qp-style';
style.textContent = `
.qp-overlay {
position: fixed; inset: 0; z-index: 9000;
background: rgba(0,0,0,.45); display: none;
}
.qp-stack {
position: fixed; top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: min(90vw, 820px); height: 80vh;
overflow: visible; box-sizing: border-box;
}
.qp-quote {
position: absolute;
top: 0; left: 0;
width: 100%; max-height: 100%;
overflow: auto;
background: #fff;
border: 1px solid #ccc;
outline: 2px solid #fff;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,.24);
padding: 18px 20px 20px;
box-sizing: border-box;
}
.qp-body .qp-content-wrap {
display: flex;
flex-direction: column;
gap: 10px;
max-width: 760px; /* 限制内容最大宽度 */
margin: 0 auto; /* 居中 */
}
.qp-body .qp-content-wrap form {
max-width: 100%; /* 表单不超过容器宽度 */
box-sizing: border-box; /* 包含内边距 */
}
.qp-body .qp-content-wrap .h-preview-box {
max-width: 100%; /* 预览框不超过容器宽度 */
overflow-wrap: break-word; /* 长单词/长链接换行 */
word-break: break-word; /* 兼容性处理 */
white-space: normal; /* 允许正常换行 */
}
.hld__docker { position: fixed; height: 80px; width: 30px; bottom: 180px; right: 0; transition: all ease .2s; z-index: 9998; }
.hld__docker:hover { width: 150px; height: 300px; bottom: 75px; }
.hld__docker-sidebar { background: #fff; position: fixed; height: 50px; width: 20px; bottom: 195px; right: 0; display: flex; justify-content: center; align-items: center; border: 1px solid #CCC; box-shadow: 0 0 1px #333; border-right: none; border-radius: 5px 0 0 5px; }
.hld__docker-btns { position: absolute; top: 0; left: 50px; bottom: 0; right: 50px; display: flex; justify-content: center; align-items: center; flex-direction: column; }
.hld__docker .hld__docker-btns>div { opacity: 0; flex-shrink: 0; }
.hld__docker:hover .hld__docker-btns>div { opacity: 1; }
.hld__docker-btns>div { background: #fff; border: 1px solid #CCC; box-shadow: 0 0 1px #444; width: 50px; height: 50px; border-radius: 50%; margin: 10px 0; cursor: pointer; display: flex; justify-content: center; align-items: center; font-size: 20px; font-weight: bold; color: #333; transition: background .2s, transform .2s; }
.hld__docker-btns>div:hover { background: #f0f0f0; transform: scale(1.1); }
`;
document.head.appendChild(style);
}
// 扩展坞 DOM
const dockerDom = $(`
<div class="hld__docker">
<div class="hld__docker-sidebar">
<svg viewBox="0 0 1024 1024" width="64" height="64">
<path d="M518.3 824.05c-7.88 0-15.76-2.97-21.69-9L215.25 533.65c-5.73-5.73-9-13.61-9-21.69s3.27-15.96 9-21.69l281.4-281.4c11.97-11.97 31.41-11.97 43.39 0s11.97 31.41 0 43.39L280.33 511.95l259.71 259.71c11.97 11.97 11.97 31.41 0 43.39-5.94 6.04-13.72 9-21.69 9z" fill="#888"/>
<path d="M787.16 772.89c-7.88 0-15.76-2.97-21.69-9L535.23 533.65c-11.97-11.97-11.97-31.41 0-43.39l230.24-230.24c11.97-11.97 31.41-11.97 43.39 0s11.97 31.41 0 43.39L600.31 511.95l208.55 208.55c11.97 11.97 11.97 31.41 0 43.39-5.94 6.04-13.72 9-21.69 9z" fill="#888"/>
</svg>
</div>
<div class="hld__docker-btns">
<div data-type="TOP">↑</div>
<div data-type="REPLY">↩</div>
<div data-type="BOTTOM">↓</div>
</div>
</div>
`);
$('body').append(dockerDom);
// 顶部按钮
dockerDom.find('[data-type="TOP"]').on('click', () => {
$('html, body').animate({ scrollTop: 0 }, 500);
});
// 悬浮窗引用
let overlay;
function ensureOverlay() {
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'qp-overlay';
overlay.innerHTML = `
<div class="qp-stack">
<div class="qp-quote">
<div class="qp-body"></div>
</div>
</div>
`;
document.body.appendChild(overlay);
// 点击遮罩关闭(点内容不关闭,且允许事件冒泡到 document 以触发引用弹窗)
overlay.addEventListener('click', function (e) {
if (e.target.closest('.qp-quote')) {
return; // 点击在内容区:不关闭,也不阻止冒泡
}
closeOverlay();
});
// ESC 关闭
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeOverlay();
});
}
return overlay;
}
function closeOverlay() {
if (!overlay) return;
overlay.style.display = 'none';
if (overlay.__formEl && overlay.__formEl.__placeholder) {
overlay.__formEl.__placeholder.parentNode.insertBefore(overlay.__formEl, overlay.__formEl.__placeholder);
if (overlay.__formEl.__wasCollapsed) {
const $form = $(overlay.__formEl);
const hint = overlay.__formEl.action.includes('doReplyThread') ? '『回复』' : '『发串』';
// 如果原位置已经有占位符,就直接隐藏并标记折叠
if ($form.prev('.xdex-placeholder').length) {
$form.hide().data('xdex-collapsed', true);
} else if (typeof Utils !== 'undefined' && typeof Utils.collapse === 'function') {
Utils.collapse($form, hint);
}
overlay.__formEl.__wasCollapsed = false;
}
}
if (overlay.__previewEl && overlay.__previewEl.__placeholder) {
overlay.__previewEl.__placeholder.parentNode.insertBefore(overlay.__previewEl, overlay.__previewEl.__placeholder);
}
}
// REPLY 按钮
dockerDom.find('[data-type="REPLY"]').on('click', () => {
let formEl = document.querySelector('form[action="/Home/Forum/doReplyThread.html"]');
let previewEl = document.querySelector('.h-preview-box');
// 如果串内没找到表单,尝试板块页发串表单
if (!formEl) {
const postForm = document.querySelector('#h-post-form form[action="/Home/Forum/doPostThread.html"]');
if (postForm) {
formEl = postForm;
previewEl = document.querySelector('#h-post-form .h-preview-box');
}
}
if (!formEl) {
toast && toast('未找到回复/发串表单');
return;
}
const ov = ensureOverlay();
const body = ov.querySelector('.qp-body');
body.innerHTML = '';
// 占位符
if (!formEl.__placeholder) {
const ph1 = document.createElement('div');
ph1.style.display = 'none';
formEl.parentNode.insertBefore(ph1, formEl);
formEl.__placeholder = ph1;
}
if (previewEl && !previewEl.__placeholder) {
const ph2 = document.createElement('div');
ph2.style.display = 'none';
previewEl.parentNode.insertBefore(ph2, previewEl);
previewEl.__placeholder = ph2;
}
// 如果表单是折叠状态,展开它(不影响原位置的占位符)
if ($(formEl).data('xdex-collapsed')) {
formEl.__wasCollapsed = true; // ★ 记录原本是折叠的
$(formEl).show().removeData('xdex-collapsed');
}
// 包装容器,防止 UI 松散
const wrap = document.createElement('div');
wrap.className = 'qp-content-wrap';
// 表单是必需的
wrap.appendChild(formEl);
// 预览框是可选的
if (previewEl) {
wrap.appendChild(previewEl);
}
body.appendChild(wrap);
// 浮窗内立即处理扩展引用,保证可点击引用弹窗
if (typeof extendQuote === 'function') {
extendQuote(previewEl || wrap);
}
// 保存引用
ov.__formEl = formEl;
ov.__previewEl = previewEl;
// 显示 overlay
ov.style.display = 'block';
// 聚焦 textarea
const ta = ov.querySelector('textarea[name="content"]');
if (ta) {
ta.focus();
const val = ta.value;
ta.value = '';
ta.value = val;
}
// Ctrl+Enter 发送并在成功后关闭浮窗
if (ta) {
const form = ta.closest('form');
if (form && !ta.__qpCtrlEnterBound) {
ta.__qpCtrlEnterBound = true;
// 保留:提交成功后关闭浮窗
document.addEventListener('replySuccess', () => {
form.__submitting = false;
closeOverlay();
}, { once: true });
// 新增:调用抽取的绑定函数
bindCtrlEnter(ta);
}
}
});
// 底部按钮:平滑滚动到最底部,不污染 URL
dockerDom.find('[data-type="BOTTOM"]').on('click', () => {
$('html, body').animate({ scrollTop: $(document).height() - $(window).height() }, 500);
});
}
function interceptReplyForm() {
// ★ 子函数:清空草稿
function 清空编辑(key) {
if (!key) key = window.location.pathname;
if (typeof GM_deleteValue === 'function') {
GM_deleteValue(key);
}
}
document.addEventListener('submit', function (e) {
const form = e.target;
const isReply = form.matches('form[action="/Home/Forum/doReplyThread.html"]');
const isPost = form.matches('form[action="/Home/Forum/doPostThread.html"]');
if (!isReply && !isPost) return;
e.preventDefault();
const formData = new FormData(form);
// 文字内容
let content = (formData.get('content') || '').toString().trim();
if (!content) {
// 检查是否选择了图片(支持 name="image")
const fileInput = form.querySelector('input[type="file"][name="image"]');
const hasImage = !!(fileInput && fileInput.files && fileInput.files.length > 0);
if (hasImage) {
// 无文字但有图片 → 自动补“分享图片”,并同步到 FormData 与 DOM
//修改以自定义。
//content = '分享图片';//默认占位文字
//content = '';//空白占位符
content = ' ';//空白占位符
formData.set('content', content);
const textarea = form.querySelector('textarea[name="content"]');
if (textarea) textarea.value = content;
} else {
toast(isReply ? '回复内容不能为空' : '发串内容不能为空');
return; // 只有没文字且没图片时才中断
}
}
fetch(form.action, {
method: form.method,
body: formData,
credentials: 'include'
})
.then(res => res.text())
.then(html => {
// 解析返回的中间页 HTML
const doc = new DOMParser().parseFromString(html, 'text/html');
const successMsg = doc.querySelector('p.success');
const errorMsg = doc.querySelector('p.error');
if (successMsg) {
toast(successMsg.textContent.trim() || (isReply ? '回复成功' : '发串成功'));
// 清空输入框
const textarea = form.querySelector('textarea[name="content"]');
if (textarea) textarea.value = '';
// 清空图片选择
const fileInput = form.querySelector('input[type="file"][name="image"]');
if (fileInput) fileInput.value = '';
// ★ 清空编辑
// 先立即删除(兜底清理,避免监听未注册导致残留)
deleteDraftSafe(getDraftKey());
// 再广播事件给增强模块/其他联动
document.dispatchEvent(new CustomEvent('replySuccess', {
detail: { key: getDraftKey() }
}));
// 重置预览框
const previewBox = document.querySelector('.h-preview-box');
if (previewBox) {
previewBox.innerHTML = `
<div class="h-threads-item">
<div class="h-threads-item-replies">
<div class="h-threads-item-reply">
<div class="h-threads-item-reply-main">
<div class="h-threads-img-box">
<div class="h-threads-img-tool uk-animation-slide-top">
<span class="h-threads-img-tool-btn h-threads-img-tool-small uk-button-link"><i class="uk-icon-minus"></i>收起</span>
<a href=":javascript:;" class="h-threads-img-tool-btn uk-button-link"><i class="uk-icon-search-plus"></i>查看大图</a>
<span class="h-threads-img-tool-btn h-threads-img-tool-left uk-button-link"><i class="uk-icon-reply"></i>向左旋转</span>
<span class="h-threads-img-tool-btn h-threads-img-tool-right uk-button-link"><i class="uk-icon-share"></i>向右旋转</span>
</div>
<a class="h-threads-img-a"><img src="" align="left" border="0" hspace="20" class="h-threads-img"></a>
</div>
<div class="h-threads-info">
<span class="h-threads-info-title">无标题</span>
<span class="h-threads-info-email">无名氏</span>
<span class="h-threads-info-createdat">2077-01-01(四)00:00:01</span>
<span class="h-threads-info-uid">ID:cookies</span>
<span class="uk-text-primary uk-text-small">(PO主)</span>
<span class="h-threads-info-report-btn">
[<a href="/f/值班室" target="_blank">举报</a>]
</span>
<a href=":javascript:;" class="h-threads-info-id" target="_blank">No.99999999</a>
</div>
<div class="h-threads-content">
无内文
</div>
</div>
</div>
</div>
</div>`;
}
// ★ 刷新饼干(不显示 toast)
if (typeof refreshCookies === 'function') {
refreshCookies(null, false);
}
if (isReply) {
refreshRepliesWithSeamlessPaging(() => {
reapplyPageEnhancements();
});
} else {
location.reload();
}
} else if (errorMsg) {
toast(errorMsg.textContent.trim() || '提交失败');
// 不刷新
} else {
toast('未知状态,可能提交失败');
}
})
.catch(() => toast('提交失败,请重试'));
}, true);
// ————— helpers —————
function getRealThreadsList(root = document) {
const lists = Array.from(root.querySelectorAll('.h-threads-list'));
return lists.find(el => !el.closest('.h-preview-box')) || null;
}
function getCurrentPage() {
const sp = new URL(location.href, location.origin).searchParams;
return parseInt(sp.get('page') || '1', 10);
}
function getMaxPageFromPagination() {
const paginations = Array.from(document.querySelectorAll('.uk-pagination.uk-pagination-left.h-pagination'));
if (!paginations.length) return null;
const last = paginations[paginations.length - 1];
let max = 1;
last.querySelectorAll('a[href*="page="]').forEach(a => {
const m = a.href.match(/[?&]page=(\d+)/);
if (m) max = Math.max(max, parseInt(m[1], 10));
});
last.querySelectorAll('li, span').forEach(el => {
const nums = (el.textContent || '').match(/\d+/g);
if (nums) nums.forEach(n => max = Math.max(max, parseInt(n, 10)));
});
return max || null;
}
function getMaxClonedPageInDOM() {
const nodes = document.querySelectorAll('.h-threads-item-replies[data-cloned-page]');
let max = 0;
nodes.forEach(el => {
const n = parseInt(el.getAttribute('data-cloned-page'), 10);
if (!isNaN(n)) max = Math.max(max, n);
});
return max;
}
function refreshRepliesWithSeamlessPaging(done) {
const currentPage = getCurrentPage();
const maxPage = getMaxPageFromPagination();
const maxCloned = getMaxClonedPageInDOM();
let targetPage = null;
if ((maxPage && maxCloned === maxPage && maxCloned > 0) || (!maxPage && maxCloned > 0)) {
targetPage = maxCloned;
} else if (maxPage && currentPage === maxPage && maxCloned === 0) {
targetPage = null;
} else {
if (typeof done === 'function') done();
return;
}
const list = getRealThreadsList(document);
if (!list) {
toast('未找到真实列表,无法刷新回复区');
if (typeof done === 'function') done();
return;
}
const targetReplies = targetPage
? list.querySelector(`.h-threads-item-replies[data-cloned-page="${targetPage}"]`)
: list.querySelector('.h-threads-item-replies:not([data-cloned-page])');
if (!targetReplies) {
toast('未找到目标回复区');
if (typeof done === 'function') done();
return;
}
let fetchUrl;
if (targetPage) {
const url = new URL(location.href, location.origin);
url.searchParams.set('page', String(targetPage));
fetchUrl = url.toString();
} else {
fetchUrl = location.href;
}
fetch(fetchUrl, { credentials: 'include' })
.then(res => res.text())
.then(html => {
const doc = new DOMParser().parseFromString(html, 'text/html');
const newList = getRealThreadsList(doc);
if (!newList) {
toast('未找到最新列表');
if (typeof done === 'function') done();
return;
}
const newReplies = newList.querySelector('.h-threads-item-replies');
if (!newReplies) {
toast('未找到最新回复区');
if (typeof done === 'function') done();
return;
}
targetReplies.innerHTML = newReplies.innerHTML;
// 新增:替换后立即重新应用标记/屏蔽
try {
const cfg2 = safeGetConfig && safeGetConfig();
if (cfg2 && typeof applyFilters === 'function') applyFilters(cfg2);
} catch (e) {}
if (typeof done === 'function') done();
})
.catch(() => {
toast('刷新回复区失败');
if (typeof done === 'function') done();
});
}
function reapplyPageEnhancements() {
const cfg = safeGetConfig();
if (!cfg) return;
if (cfg.hideEmptyTitleEmail && typeof hideEmptyTitleAndEmail === 'function') {
try {
hideEmptyTitleAndEmail();
if (window.Utils && typeof Utils.collapse === 'function') {
Utils.collapse($ && $('form[action="/Home/Forum/doReplyThread.html"]'), '『回复』');
Utils.collapse($ && $('form[action="/Home/Forum/doPostThread.html"]'), '『发串』');
Utils.collapse($ && $('.h-forum-header'), '『版规』');
}
} catch (e) {}
}
if (cfg.updateReplyNumbers && typeof updateReplyNumbers === 'function') {
try { updateReplyNumbers(); } catch (e) {}
}
if (cfg.enableHDImage && typeof enableHDImage === 'function') {
try { enableHDImage(); } catch (e) {}
}
if (cfg.extendQuote && typeof extendQuote === 'function') {
try { extendQuote(); } catch (e) {}
}
try { if (typeof applyFilters === 'function') applyFilters(cfg); } catch (e) {}
}
function safeGetConfig() {
try {
if (typeof SettingPanel !== 'undefined' && typeof GM_getValue === 'function') {
const defaults = SettingPanel.defaults || {};
const saved = GM_getValue(SettingPanel.key, {}) || {};
return Object.assign({}, defaults, saved);
}
} catch (e) {}
return null;
}
}
function extendQuote(root = document) {
const ROOT_SELECTOR = '.h-threads-content';
const QUOTE_COLOR = '#789922';
// 在容器内遍历纯文本节点,避免破坏现有标签
root.querySelectorAll(ROOT_SELECTOR).forEach(root => {
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
// 没数字直接跳过
if (!node.nodeValue || !/\d/.test(node.nodeValue)) {
return NodeFilter.FILTER_REJECT;
}
// 已经在 quote 的 <font color="#789922"> 内,跳过
const p = node.parentElement;
if (p && p.tagName.toLowerCase() === 'font' && (p.getAttribute('color') || '').toLowerCase() === QUOTE_COLOR) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
}
);
const textNodes = [];
let n;
while ((n = walker.nextNode())) textNodes.push(n);
textNodes.forEach(processTextNode);
});
function processTextNode(textNode) {
const text = textNode.nodeValue;
// 两类模式:No.12345678 与 单独 8 位数字
const patterns = [
{ name: 'no', regex: /\bNo\.(\d{8})\b/g },
{ name: 'num', regex: /(?<!\d)(\d{8})(?!\d)/g }
];
const frag = document.createDocumentFragment();
let cursor = 0;
let changed = false;
while (true) {
const next = findNextMatch(text, cursor, patterns);
if (!next) break;
const { start, end } = next;
// 若匹配前紧挨着 ">>",说明是标准引用的一部分,跳过这次,继续后移
if (start >= 2 && text[start - 2] === '>' && text[start - 1] === '>') {
// 只推进一位,继续找后面的匹配,避免卡住
cursor = start + 1;
continue;
}
// 追加匹配前的原始文本
if (start > cursor) {
frag.appendChild(document.createTextNode(text.slice(cursor, start)));
}
// 用与标准引用一致的 <font color="#789922"> 包裹,但不添加 ">>"
const font = document.createElement('font');
font.setAttribute('color', QUOTE_COLOR);
// 直接保留原始匹配文本(可能是 "No.12345678" 或 "12345678")
font.textContent = text.slice(start, end);
frag.appendChild(font);
cursor = end;
changed = true;
}
if (!changed) return;
// 末尾剩余文本
if (cursor < text.length) {
frag.appendChild(document.createTextNode(text.slice(cursor)));
}
textNode.replaceWith(frag);
}
// 在多正则间找到下一处最早的匹配
function findNextMatch(text, fromIndex, patterns) {
let best = null;
for (const p of patterns) {
p.regex.lastIndex = fromIndex;
const m = p.regex.exec(text);
if (m) {
const start = m.index;
const end = start + m[0].length;
if (!best || start < best.start) {
best = { start, end };
}
}
}
return best;
}
// === 自动定时检测 ===
if (root === document && !extendQuote.__interval) {
try {
const cfg = typeof SettingPanel !== 'undefined'
? Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}))
: { extendQuote: true };
if (cfg.extendQuote) {
extendQuote.__interval = setInterval(() => {
try {
extendQuote(); // 再次全局扫描
} catch (e) {
console.warn('extendQuote interval error', e);
}
}, 2000); // 每 2 秒检测一次
}
} catch (e) {
console.warn('extendQuote auto-scan init error', e);
}
}
}
function initExtendedContent(root) {
// root 可以是 document 或者某个新插入的 DOM 节点
$(root).find("font[color='#789922']")
.filter(function () {
// 匹配扩展格式:No.12345678 或 纯8位数字
return /(No\.\d{8}|\d{8})/.test($(this).text());
})
.off('mouseenter.ext') // 避免重复绑定
.on('mouseenter.ext', function () {
var self = this;
var tid = /\d+/.exec($(this).text())[0];
$.get('/Home/Forum/ref?id=' + tid)
.done(function (data) {
if (data.indexOf('<!DOCTYPE html><html><head>') >= 0) {
return false;
}
$("#h-ref-view").off().html(data).css({
top: $(self).offset().top,
left: $(self).offset().left
}).fadeIn(100).one('mouseleave', function () {
$(this).fadeOut(100);
});
});
});
}
function highlightPO() {
const poTextColor = '#00FFCC'; // Po 本体颜色
const iconWidthEm = 3.0; // 所有图标统一宽度
// 统一设置所有回复图标的宽度
document.querySelectorAll('.h-threads-item-reply-icon').forEach(icon => {
icon.style.display = 'inline-block';
icon.style.width = iconWidthEm + 'em';
icon.style.textAlign = 'center';
icon.style.position = 'relative';
icon.style.fontWeight = 'normal'; // 所有图标数字不加粗
});
// 替换 PO 回复的数字为 Po,并加角标
document.querySelectorAll('.h-threads-item-reply').forEach(reply => {
const main = reply.querySelector('.h-threads-item-reply-main');
const icon = reply.querySelector('.h-threads-item-reply-icon');
if (!main || !icon) return;
const isPO = !!main.querySelector('span.uk-text-primary.uk-text-small');
if (!isPO) return;
// 获取原 HTML(包含『n』)
let html = icon.innerHTML;
const m = html.match(/『(\d+)』/);
if (m) {
const originalNumber = m[1]; // 原数字
// 加粗 + 染色的 Po
const poHTML = `『<span style="color:${poTextColor}; font-weight:bold">Po</span>』`;
// 替换数字为 Po
html = html.replace(m[0], poHTML);
icon.innerHTML = html;
// 创建角标
const badge = document.createElement('span');
badge.className = 'po-n-badge';
badge.textContent = originalNumber;
Object.assign(badge.style, {
position: 'absolute',
top: '-0.55em',
right: '-0.6em',
fontSize: '10px',
lineHeight: '1',
fontWeight: '600',
color: 'initial', // 保持默认颜色
background: 'transparent',
pointerEvents: 'none',
});
icon.appendChild(badge);
}
});
}
// 初次执行
highlightPO();
// 监听 DOM 变化
const observer = new MutationObserver(() => {
highlightPO();
});
observer.observe(document.body, { childList: true, subtree: true });
function enhancePostFormLayout() {
const form = document.querySelector('form[action*="doReplyThread"], form[action*="doPostThread"]');
if (!form) return;
// 定位“标题”行与“颜文字”行
const allRows = Array.from(form.querySelectorAll('.h-post-form-grid'));
let titleRow = null, emoticonRow = null;
for (const row of allRows) {
const titleText = row.querySelector('.h-post-form-title')?.textContent?.trim() || '';
if (titleText === '标题') titleRow = row;
if (row.querySelector('.kaomoji-trigger') || row.querySelector('#h-emot-select')) {
emoticonRow = row;
}
}
// 1) 先把送出按钮移到“颜文字”行,并让整行用 flex 不换行,按钮推到行最右
if (titleRow && emoticonRow) {
const sendBtnCell = titleRow.querySelector('.h-post-form-option');
const sendBtn = sendBtnCell?.querySelector('input[type="submit"]');
if (sendBtn) {
// 让“颜文字”整行用 flex 布局,禁止换行,垂直居中
Object.assign(emoticonRow.style, {
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
width: '100%'
});
// 创建一个右侧容器,使用 margin-left:auto 将其推到最右
const btnWrapper = document.createElement('div');
Object.assign(btnWrapper.style, {
marginLeft: 'auto',
display: 'flex',
alignItems: 'center'
});
btnWrapper.appendChild(sendBtn);
// 将按钮容器添加到“颜文字”行
emoticonRow.appendChild(btnWrapper);
}
}
// 2) 折叠「回应模式 / 名 称 / E-mail / 标题」四行为一个折叠面板
// 重新抓一次,避免移动节点导致 NodeList 顺序问题
const freshRows = Array.from(form.querySelectorAll('.h-post-form-grid'));
const targets = new Set(['回应模式', '名 称', 'E-mail', '标题']);
const rowsToCollapse = [];
for (const row of freshRows) {
const label = row.querySelector('.h-post-form-title')?.textContent?.trim() || '';
if (targets.has(label)) rowsToCollapse.push(row);
}
if (rowsToCollapse.length) {
const wrapper = document.createElement('div');
wrapper.className = 'collapse-wrapper';
wrapper.style.width = '100%';
// 将折叠目标打包进容器
rowsToCollapse[0].before(wrapper);
rowsToCollapse.forEach(r => wrapper.appendChild(r));
// 使用现有 collapse 能力(依赖 jQuery)
if (typeof Utils !== 'undefined' && typeof Utils.collapse === 'function' && typeof $ === 'function') {
Utils.collapse($(wrapper), '发帖选项');
}
}
}
/* --------------------------------------------------
* 9. 颜文字增强-光标处插入/选择框优化/额外颜文字拓展
* -------------------------------------------------- */
function kaomojiEnhancer() {
// 初始化所有功能
initInsertAtCaret(); // 功能 1:颜文字插入光标处
optimizeSelectorStyle(); // 功能 2:选择框样式优化
extendKaomojiSet(); // 功能 3:颜文字样式拓展
/**
* 功能 1:选择颜文字后插入到光标位置
*/
function initInsertAtCaret() {
const SELECTOR = '#h-emot-select';
const TA_SELECTOR = 'textarea.h-post-form-textarea[name="content"]';
document.querySelectorAll(SELECTOR).forEach(select => {
if (select.dataset.kaoBound === '1') return;
select.dataset.kaoBound = '1';
const form = select.closest('form');
const textarea = form ? form.querySelector(TA_SELECTOR) : null;
if (!textarea) return;
let lastStart = 0;
let lastEnd = 0;
// 记录光标位置
const remember = () => {
lastStart = textarea.selectionStart ?? lastStart;
lastEnd = textarea.selectionEnd ?? lastEnd;
};
['keyup', 'mouseup', 'select', 'input', 'focus', 'blur'].forEach(ev =>
textarea.addEventListener(ev, remember, true)
);
['mousedown', 'pointerdown', 'touchstart', 'click'].forEach(ev =>
select.addEventListener(ev, remember, true)
);
// 拦截 change 事件,阻止原生插入逻辑
select.addEventListener('change', e => {
const val = select.value;
if (!val) return;
e.stopImmediatePropagation(); // 阻止原生事件
e.preventDefault();
insertAtCaret(textarea, val, lastStart, lastEnd);
select.value = ''; // 重置选择器
textarea.focus();
}, true); // 捕获阶段
});
function insertAtCaret(textarea, text, selStart, selEnd) {
const prevScrollTop = textarea.scrollTop;
let start = Number.isInteger(selStart) ? selStart : textarea.selectionStart;
let end = Number.isInteger(selEnd) ? selEnd : textarea.selectionEnd;
if (!Number.isInteger(start) || !Number.isInteger(end)) {
start = end = textarea.value.length;
}
const before = textarea.value.slice(0, start);
const after = textarea.value.slice(end);
textarea.value = before + text + after;
const newPos = start + text.length;
textarea.setSelectionRange(newPos, newPos);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.scrollTop = prevScrollTop;
// 更新记忆位置
lastStart = lastEnd = newPos;
}
}
/**
* 功能 2:颜文字选择框样式优化(占位)
*/
function optimizeSelectorStyle() {
const SELECTOR = '#h-emot-select';
const GAP = 4; // 单元格间距(缩小)
const CHAR_W = 14; // 每个字宽度(px)
const CHAR_H = 16; // 每个字高度(px)
const PAD = 6; // 浮窗内边距(缩小)
const ITEM_W = CHAR_W * 6 + 6; // 大约半个长颜文字宽度
const ITEM_H = CHAR_H * 2 + 4; // 不超过两行字高
const selects = document.querySelectorAll(SELECTOR);
if (!selects.length) return;
// 注入样式(只注入一次)
if (!document.getElementById('kaomoji-style')) {
const style = document.createElement('style');
style.id = 'kaomoji-style';
style.textContent = `
.kaomoji-trigger {
display: inline-flex;
align-items: center;
height: 26px;
line-height: 26px;
padding: 0 8px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
white-space: nowrap;
user-select: none;
}
.kaomoji-panel {
position: fixed;
z-index: 2147483647;
display: none;
grid-template-columns: repeat(auto-fit, minmax(${ITEM_W}px, 1fr));
gap: ${GAP}px;
padding: ${PAD}px;
border: 1px solid #ccc;
border-radius: 6px;
background: #fff;
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
box-sizing: border-box;
overflow-y: auto;
/* 关键调整 */
width: auto; /* 让宽度随内容变化 */
max-width: calc(100vw - ${PAD * 2}px); /* 不超过视口宽度 */
min-width: ${ITEM_W}px; /* 至少一列的宽度 */
max-height: calc(${ITEM_H}px * 5 + ${GAP}px * 4 + ${PAD}px * 2);
}
.kaomoji-item {
width: ${ITEM_W}px;
height: ${ITEM_H}px;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
border-radius: 4px;
cursor: pointer;
user-select: none;
text-align: center;
font-size: 14px;
line-height: 1.2;
word-break: break-word;
}
.kaomoji-item:hover {
background: #f2f2f2;
}
`;
document.head.appendChild(style);
}
selects.forEach(select => {
if (select.dataset.kaoStyled === '1') return;
select.dataset.kaoStyled = '1';
select.style.display = 'none';
const trigger = document.createElement('button');
trigger.type = 'button';
trigger.className = 'kaomoji-trigger';
trigger.textContent = '选择颜文字';
const panel = document.createElement('div');
panel.className = 'kaomoji-panel';
const options = Array.from(select.options);
options.forEach(opt => {
const item = document.createElement('div');
item.className = 'kaomoji-item';
item.textContent = opt.textContent;
item.dataset.value = opt.value;
item.addEventListener('click', () => {
select.value = opt.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
trigger.textContent = opt.textContent || '选择颜文字';
hidePanel();
});
panel.appendChild(item);
});
select.parentNode.insertBefore(trigger, select.nextSibling);
document.body.appendChild(panel);
function positionPanel() {
const rect = trigger.getBoundingClientRect();
panel.style.visibility = 'hidden';
panel.style.display = 'grid';
const panelRect = panel.getBoundingClientRect();
const panelW = panelRect.width;
const panelH = panelRect.height;
let left = rect.left;
let top = rect.top - panelH - 6; // 向上展开
const margin = 6;
if (left + panelW > window.innerWidth - margin) {
left = window.innerWidth - margin - panelW;
}
if (left < margin) left = margin;
if (top < margin) {
top = rect.bottom + 6; // 空间不足则向下展开
}
panel.style.left = `${Math.round(left)}px`;
panel.style.top = `${Math.round(top)}px`;
panel.style.visibility = '';
}
function showPanel() {
positionPanel();
panel.style.display = 'grid';
bindGlobalClose();
}
function hidePanel() {
panel.style.display = 'none';
unbindGlobalClose();
}
trigger.addEventListener('click', (e) => {
e.stopPropagation();
if (panel.style.display === 'none' || panel.style.display === '') {
showPanel();
} else {
hidePanel();
}
});
let outsideHandler, escHandler;
function bindGlobalClose() {
outsideHandler = (e) => {
// 捕获阶段执行,防止被其他脚本阻止
const target = e.target;
if (!panel.contains(target) && target !== trigger) {
hidePanel();
}
};
escHandler = (e) => {
if (e.key === 'Escape') hidePanel();
};
// 用捕获阶段监听 click 和 mousedown
document.addEventListener('click', outsideHandler, true);
document.addEventListener('mousedown', outsideHandler, true);
window.addEventListener('keydown', escHandler);
}
function unbindGlobalClose() {
document.removeEventListener('click', outsideHandler, true);
document.removeEventListener('mousedown', outsideHandler, true);
window.removeEventListener('keydown', escHandler);
}
select.addEventListener('kaomoji:updated', () => {
// 清空并重建 panel 子项
while (panel.firstChild) panel.removeChild(panel.firstChild);
Array.from(select.options).forEach(opt => {
const item = document.createElement('div');
item.className = 'kaomoji-item';
item.textContent = opt.textContent;
item.dataset.value = opt.value;
item.addEventListener('click', () => {
select.value = opt.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
trigger.textContent = opt.textContent || '选择颜文字';
panel.style.display = 'none';
});
panel.appendChild(item);
});
});
});
}
/**
* 功能 3:颜文字样式拓展(占位)
*/
function extendKaomojiSet() {
const SELECTOR = '#h-emot-select';
const EXTRA_EMOTS = [
" ҉ \n( ゚∀。)","( ´_ゝ`)旦","(<ゝω・) ☆","(`ε´ (つ*⊂)","=͟͟͞͞( 'ヮ' 三 'ヮ' =͟͟͞͞)","↙(`ヮ´ )↗ 开摆!",
"(っ˘Д˘)ノ<","(ノ#)`д´)σ","₍₍(ง`ᝫ´ )ว⁾","( `ᵂ´)","( *・ω・)✄╰ひ╯","U•ェ•*U","⊂( ゚ω゚)つ",
"( ゚∀。)7","・゚( ゚∀。) ゚。","( `д´)σ","( ゚ᯅ 。)","( ;`д´; )","m9( `д´)","( ゚π。)","ᕕ( ゚∀。)ᕗ",
"ฅ(^ω^ฅ)","(|||^ヮ^)","(|||ˇヮˇ)","( ↺ω↺)"," `ー´) `д´) `д´)","接☆龙☆大☆成☆功",
"ᑭ`д´)ᓀ ∑ᑭ(`ヮ´ )ᑫ","乚 (^ω^ ミэ)Э好钩我咬","乚(`ヮ´ ミэ)Э","( ゚∀。ミэ)Э三三三三 乚",
"(ˇωˇ ミэ)Э三三三三 乚","( へ ゚∀゚)べ摔低低","(ベ ˇωˇ)べ 摔低低"
];
const EXTRA_RICH = {
"齐齐蛤尔": "(`ヮ´ )σ`∀´) ゚∀゚)σ",
"呼伦悲尔": "( ノд`゚);´д`) ´_ゝ`)",
"愕尔多厮": "Σ( ゚д゚)´゚Д゚) ゚д゚)))",
"智利": "( ゚∀。)∀。)∀。)",
"阴山山脉": "( ˇωˇ )◕∀◕。)^ω^)",
"F5欧拉": " σ σ\nσ( ´ρ`)σ[F5]\n σ σ",
"UK酱": "\\ ︵\nᐕ)⁾⁾",
"白羊": "╭◜◝ ͡ ◜◝ J J\n( `д´) “咩!”\n╰◟д ◞ ͜ ◟д◞",
"兔兔": " /) /)\nc( ╹^╹)",
"neko": " ∧,, \n ヾ `. 、`フ\n (,`'´ヽ、、ヅ\n (ヽv' `''゙つ\n ,ゝ ⌒`y'''´\n ( (´^ヽこつ\n ) )\n (ノ",
"给你": "(\\_/)\n(・_・)\n / >",
"举高高": " _∧_∧_ \n ((∀`/ ) \n /⌒ / \n /(__ノ\_ノ \n (_ノ ||| 举高高~~\n ∧_∧ ∧_∧\n(( ・∀・ ))・∀・) )\n`\ ∧ ノ\n / |/ |\n(_ノ_)_ノL_)",
"举糕糕": "举糕糕~\n ☆☆☆☆☆☆☆☆\n ╭┻┻┻┻┻┻┻┻╮\n ┃╱╲╱╲╱╲╱╲┃\n╭┻━━━━━━━━┻╮\n┃╱╲╱╲╱╲╱╲╱╲┃\n┗━━━━━━━━━━┛\n ∧_∧ ∧_∧\n (( ・∀・ ))・∀・) )\n `\ ∧ ノ\n / |/ |\n (_ノ_)_ノL_)",
"Happy肥肥Day": ". .★ * ★.. \n .*★ *. *..* ★ \n ★ ★ \n ‘*. *' ʜᴀᴘᴘʏ 肥肥 ᴅᴀʏ \n ‘★. ★’ \n *..★\n┊┊┊┊☆☆☆☆☆☆☆☆┊┊┊┊\n┊┊┊╭┻┻┻┻┻┻┻┻╮┊┊┊\n┊┊┊┃╱╲╱╲╱╲╱╲┃┊┊┊\n┊┊╭┻━━━━━━━━┻╮┊┊\n┊┊┃╱╲╱╲╱╲╱╲╱╲┃┊┊\n╱▔┗━━━━━━━━━━┛▔╲",
"举高高2": " _∧_∧_\n ((∀`/ )\n /⌒ /\n /(__ノ\_ノ(((ヽ\n (_ノ  ̄Y\\n| (\ (\\ /) | )举高高!\nヽ ヽ` ( ゚∀゚ ) _ノ /\n \ | ⌒Y⌒ / /\n |ヽ · | · ノ //\n \トー仝ーイ\n | ミ土彡/\n ). \\ ° /\n ( \\. y \\",
"大嘘": "吁~~~~ rnm,退钱!\n / /\n( ゚ 3゚) `ー´) `д´) `д´)",
"催更喵gkd": " ___\n /> フ\n | _ _ l 我是一只催更的\n /` ミ_xノ 喵喵酱\n / | gkdgkd\n / ヽ ノ\n │ | | |\n / ̄| | | |\n | ( ̄ヽ__ヽ_)__)\n \二つ",
"巴拉巴拉": " ∧_∧\n(。・ω・。)つ━☆・*。\n⊂ ノ ・゜+.\n しーJ °。+ *´¨)\n .· ´¸.·*´¨) ¸.·*¨)\n (¸.·´ (¸.·’*",
"碣石": " _ _\n ( ゚_゚)\n/ (⁰ )╲/",
"冰封王座": "(ノ゚∀゚)ノ👑\n ( ゚∀。)\n\n 👑\n(//゚ω゚)//\n \n 👑\n(Ⅱ゚ω゚)Ⅱ\n\n 👑\nᕕ( ᐛ )ᕗ",
"冰封王座2": "(ノ゚∀゚)ノ🍟\n ( ゚∀。)\n\n 🍟\n(//゚ω゚)//\n \n 🍟\n(Ⅱ゚ω゚)Ⅱ\n\n 🍟\nᕕ( ᐛ )ᕗ",
"冰封王座3": "( `д´)=🔪 👑\n ( ゚ 3゚)\n 👑\n(// `ー´)//\n( x 3x)\n 👑\n(Ⅱ`∀´)Ⅱ\n( x 3x)\n 👑\n( `д´)\n( x 3x)",
"全角空格": " "
};
const ORDERED_RICH = [
"齐齐蛤尔","呼伦悲尔","愕尔多厮","智利","阴山山脉",
"F5欧拉","UK酱","白羊","兔兔","neko","给你",
"举高高","举糕糕","Happy肥肥Day","举高高2",
"大嘘","催更喵gkd","巴拉巴拉","碣石",
"冰封王座","冰封王座2","冰封王座3","全角空格"
// 页面中“防剧透/骰子/高级骰子”不动其原位
];
const NEED_LF = new Set([
"F5欧拉","UK酱","白羊","兔兔","neko","给你",
"举高高","举糕糕","Happy肥肥Day","举高高2",
"大嘘","催更喵gkd","巴拉巴拉","碣石","冰封王座","冰封王座2","冰封王座3"
]);
// 一次性补齐(选择器就绪且已有至少一个选项时调用)
function patchSelect(sel) {
if (!sel || sel.dataset.kaoExtended === '1') return;
// 去重集合
const existingValues = new Set();
const existingLabels = new Set();
Array.from(sel.options).forEach(op => {
existingValues.add(op.value);
existingLabels.add(op.textContent);
});
// 追加普通
const frag = document.createDocumentFragment();
EXTRA_EMOTS.forEach(txt => {
if (!existingValues.has(txt)) {
frag.appendChild(new Option(txt, txt));
existingValues.add(txt);
}
});
if (frag.childNodes.length) sel.appendChild(frag);
// 收集需要重排的富颜文字:先把 ORDERED_RICH 中已存在的挪出
const bucket = new Map(); ORDERED_RICH.forEach(k => bucket.set(k, null));
for (let i = sel.options.length - 1; i >= 0; i--) {
const op = sel.options[i];
const label = op.textContent;
if (bucket.has(label)) {
bucket.set(label, op);
sel.remove(i);
}
}
// 按顺序插回;缺的用 EXTRA_RICH 补齐;值前加换行(NEED_LF)
const richFrag = document.createDocumentFragment();
ORDERED_RICH.forEach(key => {
let node = bucket.get(key);
if (node) {
richFrag.appendChild(node);
} else if (EXTRA_RICH[key]) {
const val = NEED_LF.has(key) ? ("\n" + EXTRA_RICH[key] + "\n") : EXTRA_RICH[key];
richFrag.appendChild(new Option(key, val));
}
});
if (richFrag.childNodes.length) sel.appendChild(richFrag);
sel.dataset.kaoExtended = '1';
sel.dispatchEvent(new CustomEvent('kaomoji:updated', { bubbles: true }));
}
// 方案 A:钩住 jQuery 的 append,仅针对 #h-emot-select
(function hookjQueryAppend(){
const $ = window.jQuery;
if (!$ || !$.fn || !$.fn.append || $.fn.append.__kaoHooked) return;
const rawAppend = $.fn.append;
$.fn.append = function(...args){
const ret = rawAppend.apply(this, args);
try {
// 如果目标包含我们的 select,就尝试打补丁
this.each(function(){
if (this && this.querySelector) {
const sel = this.matches && this.matches(SELECTOR) ? this : this.querySelector(SELECTOR);
if (sel) patchSelect(sel);
}
});
} catch(_) {}
return ret;
};
$.fn.append.__kaoHooked = true;
})();
// 方案 B:用 MutationObserver 监听 select 的子节点变化,首次填充后补齐
(function observeSelect(){
const sel = document.querySelector(SELECTOR);
if (!sel) return;
// 若已有人填充过,直接打一次补丁
if (sel.options && sel.options.length > 0) {
patchSelect(sel);
}
// 监听后续填充
const mo = new MutationObserver(() => {
// 一旦看到有 option 节点,就补丁并停止观察
if (sel.options && sel.options.length > 0) {
patchSelect(sel);
mo.disconnect();
}
});
mo.observe(sel, { childList: true, subtree: false });
})();
// 方案 C:兜底重试(避免异步加载错过时机)
let tries = 0;
(function retry(){
const sel = document.querySelector(SELECTOR);
if (sel && sel.options && sel.options.length > 0) {
patchSelect(sel);
return;
}
if (tries++ < 30) setTimeout(retry, 100);
})();
}
}
function renderSpoiler(container) {
var $container = $(container);
// 用构造函数写法,避免 /^.../ 形式
var reg = new RegExp("\\[h\\]([\\s\\S]*?)\\[\\/h\\]", "gi");
$container.find('.h-threads-content').each(function () {
var text = $(this).text().trim();
var match = reg.exec(text);
if (match) {
$(this).html('<span class="h-hidden-text">' + match[1] + '</span>');
}
});
}
/* --------------------------------------------------
* 10. ‘增强x岛匿名版’:添加预览框+草稿保存/恢复/自动设置网页标题/人类友好时间显示/引用追记/粘贴图片上传
* -------------------------------------------------- */
// 统一生成草稿存储用的 key
// 统一:草稿 key 生成
function getDraftKey() {
return window.location.pathname;
}
// 统一:安全删除草稿(有 GM_deleteValue 用之;否则写空串兜底)
function deleteDraftSafe(key) {
try {
if (!key) key = getDraftKey();
if (typeof GM_deleteValue === 'function') {
GM_deleteValue(key);
} else if (typeof GM_setValue === 'function') {
GM_setValue(key, ''); // 覆盖为空字符串
} else {
// 无 GM_* 时不做处理
}
} catch (_) {}
}
// 完整移植为可调用函数。需要:jQuery 2.2.4+;GM_setValue/GM_getValue/GM_deleteValue 授权
function enhanceIsland(config = {}) {
// 配置开关(默认全开)
const cfg = Object.assign({
enablePreview: true, // 发帖预览(插入预览DOM并实时渲染)
enableDraft: true, // 草稿保存/恢复和成功后清理
enableAutoTitle: true, // 自动设置网页标题(含页码)
enableRelativeTime: true, // 相对时间显示(每2.5秒刷新)
enableQuoteInsert: true, // 点击 No.xxxx 插入引用
enablePasteImage: true // 粘贴剪贴板图片到文件输入
}, config);
// 解析 jQuery
const $ = cfg.$ || window.jQuery || window.$;
if (!$) {
console.warn('[enhanceIsland] jQuery not found.');
return;
}
// 公用变量
const 正文框 = $('textarea.h-post-form-textarea');
const search = window.location.search;
const 搜索参数 = {};
search.replace(/^\?/, '').split('&').forEach(kev => {
if (!kev) return;
const [k, v] = kev.split('=', 2);
搜索参数[k] = v;
});
const 路径 = window.location.pathname;
const 路径分块 = 路径.split('/').splice(1);
// 预览区域 DOM(与原脚本一致)
const previewHtml = `
<div class="h-preview-box">
<div class="h-threads-item">
<div class="h-threads-item-replies">
<div class="h-threads-item-reply">
<div class="h-threads-item-reply-main">
<div class="h-threads-img-box">
<div class="h-threads-img-tool uk-animation-slide-top">
<span class="h-threads-img-tool-btn h-threads-img-tool-small uk-button-link"><i class="uk-icon-minus"></i>收起</span>
<a href=":javascript:;" class="h-threads-img-tool-btn uk-button-link"><i class="uk-icon-search-plus"></i>查看大图</a>
<span class="h-threads-img-tool-btn h-threads-img-tool-left uk-button-link"><i class="uk-icon-reply"></i>向左旋转</span>
<span class="h-threads-img-tool-btn h-threads-img-tool-right uk-button-link"><i class="uk-icon-share"></i>向右旋转</span>
</div>
<a class="h-threads-img-a"><img src="" align="left" border="0" hspace="20" class="h-threads-img"></a>
</div>
<div class="h-threads-info">
<span class="h-threads-info-title">无标题</span>
<span class="h-threads-info-email">无名氏</span>
<span class="h-threads-info-createdat">2077-01-01(四)00:00:01</span>
<span class="h-threads-info-uid">ID:cookies</span>
<span class="uk-text-primary uk-text-small">(PO主)</span>
<span class="h-threads-info-report-btn">
[<a href="/f/值班室" target="_blank">举报</a>]
</span>
<a href=":javascript:;" class="h-threads-info-id" target="_blank">No.99999999</a>
</div>
<div class="h-threads-content">
无内文
</div>
</div>
</div>
</div>
</div>
</div>
`;
// 只有在页面存在发帖表单容器时才插入预览
function initPreviewBox() {
if (!cfg.enablePreview) return;
if (!$('#h-post-form form').length) return;
// 只创建一次预览框
if (!$('.h-preview-box').length) {
const $box = $(previewHtml).insertAfter('#h-post-form form');
// 初始化时同步饼干 ID
const cookieDisplay = document.querySelector('#current-cookie-display');
if (cookieDisplay) {
const cookieText = cookieDisplay.textContent.trim();
$box.find('.h-threads-info-uid').text(`ID:${cookieText}`);
}
// === 图片预览更新函数 ===
function updatePreviewFromFile(file) {
const imgEl = $box.find('.h-threads-img')[0];
const imgLink = $box.find('.h-threads-img-a')[0];
if (!imgEl) return;
// 清理旧 URL
if (imgEl.dataset.prevObjectUrl) {
URL.revokeObjectURL(imgEl.dataset.prevObjectUrl);
delete imgEl.dataset.prevObjectUrl;
}
if (file) {
const objectUrl = URL.createObjectURL(file);
imgEl.src = objectUrl;
imgEl.dataset.prevObjectUrl = objectUrl;
imgEl.style.display = 'block';
if (imgLink) imgLink.href = objectUrl;
} else {
imgEl.src = '';
imgEl.style.display = 'none';
if (imgLink) imgLink.removeAttribute('href');
}
}
// === 监听文件选择 ===
const fileInput = document.querySelector('input[type="file"][name="image"]');
if (fileInput) {
fileInput.addEventListener('change', function () {
const file = this.files && this.files[0] ? this.files[0] : null;
updatePreviewFromFile(file);
});
}
// === 监听粘贴图片(不依赖 change 事件)===
document.addEventListener('paste', function (e) {
const items = (e.clipboardData || e.originalEvent?.clipboardData)?.items || [];
if (!items.length) return;
let file = null;
for (const it of items) {
if (it.kind === 'file') {
const f = it.getAsFile();
if (f && f.type.startsWith('image/')) {
file = f;
break;
}
}
}
if (!file) return;
// 如果 input 存在,也同步到 input.files
if (fileInput) {
try {
const dt = new DataTransfer();
dt.items.add(file);
fileInput.files = dt.files;
} catch (_) {
// 某些浏览器不支持 DataTransfer 构造器
}
}
// 直接更新预览
updatePreviewFromFile(file);
// 触发 change,让绑定在 file input 上的逻辑(如清除按钮)也能执行
if (fileInput) {
try {
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
} catch (_) {}
}
}, true);
}
}
// 预览引用/隐藏文本渲染
const previewBox = $('<div/>'); // 占位,真正引用在 initPreviewBox 后重新抓取
const refExp = /^([>>]+.*)$/g;
const hideExp = /\[h\](.*)\[\/h\]/g;
function renderContent(raw) {
const box = $('.h-preview-box');
if (!box.length) return;
const previewContent = box.find('.h-threads-content');
if (typeof raw !== 'string' || raw.trim() === '') {
previewContent.text('');
return;
}
previewContent.text('');
for (let i of raw.split('\n')) {
i = i.replace(/ +/g, ' ');
let e;
if (refExp.test(i)) {
e = $('<font color="#789922"></font>').text(i);
} else if (hideExp.test(i)) {
e = $('<span class="h-hidden-text"></span>').text(i);
} else {
e = $('<span></span>').text(i);
}
previewContent.append(e);
previewContent.append('<br>');
// 支持拓展引用:把扩展引用包上 <font color="#789922">,以便可点击弹窗
if (typeof extendQuote === 'function') {
extendQuote(previewContent[0]);
}
}
}
// 草稿:发帖成功清空(拦截模式优先)
function 清空编辑(key) {
if (!cfg.enableDraft) return;
if (key) {
deleteDraftSafe(key);
return;
}
// ===== 兼容原“中间页”的旧逻辑(仍保留)=====
const okBox = document.getElementsByClassName('success')[0];
if (!okBox) return;
if (!okBox.textContent.includes('回复成功')) return;
const hrefEl = document.getElementById('href');
if (!hrefEl || !hrefEl.href) return;
const m = /https?:\/\/[^/]+(\/t\/\d+)/.exec(hrefEl.href);
if (!m) return;
deleteDraftSafe(m[1]);
}
// 统一用事件清空,缺省用 getDraftKey()
document.addEventListener('replySuccess', e => {
清空编辑(e.detail?.key || getDraftKey());
});
// 草稿:载入
function 载入编辑() {
if (!cfg.enableDraft) return;
if (!正文框.length) return;
const key = window.location.pathname;
let draft = '';
if (typeof GM_getValue === 'function') {
draft = GM_getValue(key, '');
}
// 检查 URL 参数 r
if (搜索参数.r) {
const quote = `>>No.${搜索参数.r}\n`;
if (!draft.startsWith(quote)) {
draft = quote + draft;
}
}
正文框.val(draft);
}
// 草稿:注册自动保存 + 初始化一次保存触发(原脚本用 $(保存编辑))
function 注册自动保存编辑() {
if (!cfg.enableDraft) return;
$('form').on('input', 保存编辑);
// 文档就绪时同步一次
$(保存编辑);
}
function 保存编辑() {
if (!cfg.enableDraft) return;
if (!正文框.length) return;
if (typeof GM_setValue === 'function') {
GM_setValue(window.location.pathname, 正文框.val());
}
renderContent(正文框.val());
const box = $('.h-preview-box');
if (box.length) {
const previewTitle = box.find('.h-threads-info-title');
const previewEmail = box.find('.h-threads-info-email');
previewTitle.text($('form input[name="title"]').val() || '无标题');
previewEmail.text($('form input[name="name"]').val() || '无名氏');
}
}
// 点击 No.xxxx 插入引用(保持原先光标与选择区逻辑)
function 注册追记引用串号() {
if (!cfg.enableQuoteInsert) return;
$('body').on('click', 'a.h-threads-info-id', e => {
if (!正文框.length) return;
const start = 正文框.prop('selectionStart');
const end = 正文框.prop('selectionEnd');
const len = end - start;
const str = 正文框.val();
const left = str.substring(0, start);
const right = str.substring(end);
const ref = `>>${e.target.textContent.trim()}`;
正文框.val(
start === 0
? `${ref}\n${right}`
: end === str.length
? `${left}\n${ref}\n`
: len > 0
? `${left} ${ref} ${right}`
: `${left}\n${ref}`
);
正文框.trigger('input', '');
保存编辑();
e.preventDefault();
});
}
// 粘贴图片到文件输入(保持原选择器)
function 注册粘贴图片() {
if (!cfg.enablePasteImage) return;
window.addEventListener('paste', e => {
const files = (e.clipboardData || e.originalEvent?.clipboardData)?.files || [];
if (files.length) {
const fileInput = document.querySelector('input[type="file"][name="image"]');
if (fileInput) fileInput.files = files;
}
});
}
// 子函数:选择了图片后出现“清除图片”按钮;清除后按钮消失,恢复“选择文件”
function 绑定清除图片按钮() {
const $form = $('#h-post-form form, form[action="/Home/Forum/doReplyThread.html"]').first();
if (!$form.length) return;
const $file = $form.find('input[type="file"][name="image"]');
if (!$file.length) return;
if ($file.data('xdexClearBound')) return;
$file.data('xdexClearBound', true);
// 包一层容器,方便布局
if (!$file.parent().hasClass('xdex-file-wrapper')) {
$file.wrap('<div class="xdex-file-wrapper" style="display:flex;align-items:center;justify-content:space-between;width:100%;"></div>');
}
function 刷新按钮() {
const hasFile = $file[0].files && $file[0].files.length > 0;
let $btn = $form.find('.xdex-clear-image-btn');
if (hasFile) {
if (!$btn.length) {
$btn = $('<button type="button" class="xdex-clear-image-btn" title="清除图片">×</button>');
$btn.css({
fontSize: '16px',
lineHeight: '1',
padding: '2px 6px',
cursor: 'pointer'
});
$file.after($btn);
$btn.on('click', function (e) {
e.stopPropagation(); // 防止触发浮窗关闭
e.preventDefault();
// 清空文件
$file.val('');
// 直接调用预览清空逻辑
if (typeof updatePreviewFromFile === 'function') {
updatePreviewFromFile(null);
} else {
// 兜底:如果函数不可用,就清空 src/href 但不移除节点
const $preview = $('.h-preview-box');
$preview.find('img').attr('src', '').removeAttr('src');
$preview.find('.h-threads-img-a').attr('href', '');
}
// 移除按钮
$(this).remove();
});
}
} else {
if ($btn.length) $btn.remove();
}
}
$file.on('change', 刷新按钮);
刷新按钮();
}
// 递归访问 DOM
function visit(root, cb) {
if (!root) return;
if (cb(root) === '停止') return;
for (const child of root.children || []) {
visit(child, cb);
}
}
// 自动标题:择标题(与原逻辑等价)
function 选择标题() {
const titleEl = document.querySelector('.h-threads-list .h-threads-item-main .h-threads-info .h-threads-info-title');
const contentEl = document.querySelector('.h-threads-list .h-threads-item-main .h-threads-content');
if (!contentEl) return document.title;
const titleText = (titleEl?.textContent || '').trim();
if (titleText && titleText !== '无标题') return titleText;
const redTexts = [];
visit(contentEl, el => {
try {
if (window.getComputedStyle(el).color === 'rgb(255, 0, 0)') {
const redSegment = el.textContent.replace(/^[=\s+]+|[=\s+]+$/, '');
if (redSegment !== '') redTexts.push(redSegment);
return '停止';
}
} catch (_) {}
});
const red = redTexts.join('');
if (red !== '') return red;
const lines = (contentEl.innerText || '').split('\n');
for (let line of lines) {
if ((line = line.trim()) !== '') return line;
}
return document.title;
}
function 自动标题() {
if (!cfg.enableAutoTitle) return;
const 页码 = 路径分块[0] === 'Forum'
? (路径分块[5]?.replace(/\.html$/, '') || 1)
: (搜索参数.page || 1);
const 标题 = 选择标题();
const titleEl = document.querySelector('title');
if (titleEl) {
titleEl.textContent = `${标题} - ${titleEl.textContent} - page ${页码}`;
}
}
// 相对时间格式化(与原逻辑等价,目标 span.h-threads-info-createdat)
function getFriendlyTime(machineReadableTime) {
const date = new Date(machineReadableTime);
const now = new Date();
if (now < date) return machineReadableTime;
let friendlyDate = machineReadableTime.slice(0, 10);
let friendlyTime = machineReadableTime.slice(13, 21);
const weekday = machineReadableTime.slice(11, 12);
const diff = (now.getTime() - date.getTime()) / 1000;
if (diff < 60) {
friendlyTime = `${Math.floor(diff)}秒前`;
} else if (diff < 3600) {
friendlyTime = `${Math.floor(diff / 60)}分钟前`;
} else if (diff < 24 * 3600) {
friendlyTime = `${Math.floor(diff / 3600)}小时前 ${friendlyTime}`;
}
const yesterday = new Date(new Date(now - 1000 * 60 * 60 * 24).toLocaleDateString());
if (now.toLocaleDateString() === date.toLocaleDateString()) {
friendlyDate = '今天';
} else if (yesterday.toLocaleDateString() === date.toLocaleDateString()) {
friendlyDate = '昨天';
} else if (yesterday - date < 1000 * 60 * 60 * 24 * 30) {
friendlyDate = `${Math.floor((now - date) / (1000 * 60 * 60 * 24))}天前`;
} else if (now.getFullYear() === date.getFullYear()) {
friendlyDate = friendlyDate.slice(5);
} else {
friendlyDate = `${now.getFullYear() - date.getFullYear()}年前 ${friendlyDate}`;
}
return `${friendlyDate}(${weekday})${friendlyTime}`;
}
function formatDateStrOnPage() {
if (!cfg.enableRelativeTime) return;
const targets = $('span.h-threads-info-createdat');
targets.each(function () {
const target = $(this);
const timeStr = target.attr('title') || target.text().trim();
if (!timeStr) return;
target.attr('title', timeStr);
const friendlyTime = getFriendlyTime(timeStr);
target.text(friendlyTime);
});
}
// 路由:各页面初始化(与原逻辑一致)
function 串() {
if (cfg.enablePreview) initPreviewBox();
if (cfg.enableDraft) 载入编辑();
if (cfg.enableQuoteInsert) 注册追记引用串号();
if (cfg.enableDraft) 注册自动保存编辑();
if (cfg.enablePasteImage) 注册粘贴图片();
绑定清除图片按钮();
if (cfg.enableAutoTitle) 自动标题();
if (cfg.enableRelativeTime) setInterval(formatDateStrOnPage, 2500);
}
function 串只看po() {
if (cfg.enableAutoTitle) 自动标题();
}
function 版块() {
if (cfg.enablePreview) initPreviewBox();
if (cfg.enableDraft) 注册自动保存编辑();
//if (cfg.enableQuoteInsert) 注册追记引用串号();
if (cfg.enablePasteImage) 注册粘贴图片();
绑定清除图片按钮();
if (cfg.enableRelativeTime) setInterval(formatDateStrOnPage, 2500);
}
function 回复成功() {
if (cfg.enableDraft) 清空编辑();
if (cfg.enablePasteImage) 注册粘贴图片();
}
document.addEventListener('replySuccess', e => {
回复成功(e.detail?.key);
});
function 未知() {
if (cfg.enablePasteImage) 注册粘贴图片();
}
// 一级路径解析(支持 m 前缀)
const 一层路径 = 路径分块[0] === 'm' ? 路径分块[1] : 路径分块[0];
// 入口分流(与原脚本一致)
switch (一层路径) {
case 't':
串();
break;
case 'f':
版块();
break;
case 'Forum':
if (路径分块[1] === 'po' && 路径分块[2] === 'id') { 串只看po(); } else { 未知(); }
break;
case 'Home':
if (路径 === '/Home/Forum/doReplyThread.html') { 回复成功(); } else { 未知(); }
break;
case 'Member':
if (路径.startsWith('/Member/User/Cookie/export/id/')) { console.debug('//TODO'); }
break;
default:
未知();
}
// 首次渲染预览(若需要)
if (cfg.enablePreview) {
renderContent(正文框.val ? (正文框.val() || '') : '');
}
}
/* --------------------------------------------------
* 11. 入口初始化
* -------------------------------------------------- */
$(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'), '『版规』');
Utils.collapse($('form[action="/Home/Forum/doReplyThread.html"]'), '『回复』');
Utils.collapse($('form[action="/Home/Forum/doPostThread.html"]'), '『发串』');
} //折叠版规-发串
if (cfg.enableExternalImagePreview) ExternalImagePreview.init();//显示外部图床
if (cfg.updateReplyNumbers) updateReplyNumbers(); //添加回复编号
if (cfg.enableSeamlessPaging) initSeamlessPaging(); //自动-手动无缝翻页
if (cfg.enableHDImage) enableHDImage(); //X岛-揭示板的增强型体验-高清图片链接+图片控件
if (cfg.enableLinkBlank) runLinkBlank(); //X岛-揭示板的增强型体验-新标签打开串
if (cfg.enableQuotePreview) enableQuotePreview(); //优化引用弹窗
replaceRightSidebar(); //扩展坞增强
interceptReplyForm(); //拦截回复中间页
if (cfg.extendQuote) extendQuote(); //扩展引用格式
kaomojiEnhancer(); //颜文字拓展
highlightPO(); //高亮PO主
enhancePostFormLayout(); //发帖UI调整
applyFilters(cfg); //标记/屏蔽/过滤-饼干/关键词
initExtendedContent(); //扩展引用
autoHideRefView();
enhanceIsland({ //增强X岛匿名版
// 这些都可选,默认全开
enablePreview: true, //添加预览框
enableDraft: true, //草稿保存/恢复
enableAutoTitle: true, //自动设置网页标题
enableRelativeTime: true, //人类友好时间显示
enableQuoteInsert: true, //点击 No.xxxx 插入引用
enablePasteImage: true, //粘贴剪贴板图片到文件输入
// 可传入你的 jQuery 实例(若页面没有全局 $)
// $: window.myJQ
});
// 保存原始函数
const _initContent = window.initContent;
// 将X岛原版https://www.nmbxd1.com/Public/Js/h.desktop.js中作用于引用的initContent函数支持我们新增的非标准引用格式
window.initContent = function(root) {
// 先执行原始逻辑
if (typeof _initContent === 'function') {
_initContent(root);
}
// 再执行扩展逻辑
initExtendedContent(root || document);
};
initContent(document);
document.querySelectorAll('form textarea[name="content"]').forEach(bindCtrlEnter);
// 自动刷新并提示当前饼干,我不知道为什么必须写在这里。
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);