// ==UserScript== // @name 论坛帖子文字自动放大器 // @namespace https://scriptcat.org/ // @version 0.1.1 // @description 自动识别论坛帖子正文与回复区域,安全放大字号。支持全局保底增量与当前域名独立增量设置、可视化点击拾取选择器。 // @author gymimc // @match *://*/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; // 默认配置常量 const CONFIG_KEYS = { GLOBAL_DELTA: 'config_global_font_delta', // 全局通用增量 key DOMAIN_DELTA_PREFIX: 'font_delta_', // 域名特定增量 key 前缀 DOMAIN_SELECTOR_PREFIX: 'selector_', // 【修改】补全末尾的逗号 DOMAIN_DISABLED_PREFIX: 'disabled_' // 当前域名禁用标志 }; // ========================================== // 【新增】判断与设置当前域名是否禁用的函数 // ========================================== /** * 判断当前域名是否已被禁用 * @returns {boolean} */ function isDomainDisabled() { return GM_getValue(`${CONFIG_KEYS.DOMAIN_DISABLED_PREFIX}${window.location.hostname}`, false) === true; } /** * 切换当前域名的禁用状态 * @param {boolean} disabled */ function setDomainDisabled(disabled) { GM_setValue(`${CONFIG_KEYS.DOMAIN_DISABLED_PREFIX}${window.location.hostname}`, disabled); } const DEFAULT_CONFIG = { fontDelta: 3, // 默认全局增量 (像素/px) lineHeight: '1.6', // 默认自适应行高 }; // 1. 预设常用论坛选择器 const COMMON_SELECTORS = [ '.post-message', '.topic-content', '.markdown-body', '[class*="post-content"]', '[class*="thread-content"]', 'td.t_f', '.reply-content', '.c_post_content', '.article-content', '.entry-content', '.post-text' ]; /** * 读取全局通用字号增量 * @returns {number} */ function getGlobalFontDelta() { return parseInt(GM_getValue(CONFIG_KEYS.GLOBAL_DELTA, DEFAULT_CONFIG.fontDelta), 10); } /** * 读取当前域名的专属字号增量(若未设置,则返回 null) * @returns {number|null} */ function getDomainFontDelta() { const val = GM_getValue(`${CONFIG_KEYS.DOMAIN_DELTA_PREFIX}${window.location.hostname}`, null); return val !== null ? parseInt(val, 10) : null; } /** * 获取最终生效的字号增量(域名优先级 > 全局优先级) * @returns {number} */ function getEffectiveFontDelta() { const domainDelta = getDomainFontDelta(); if (domainDelta !== null && !isNaN(domainDelta)) { return domainDelta; } return getGlobalFontDelta(); } /** * 获取当前域名的自定义 CSS 选择器 * @returns {string} */ function getDomainSelector() { return GM_getValue(`${CONFIG_KEYS.DOMAIN_SELECTOR_PREFIX}${window.location.hostname}`, ''); } /** * 智能识别论坛正文节点 * @returns {Element[]} */ function findContentNodes() { // 策略 0:优先匹配当前域名的自定义选择器 const customSelector = getDomainSelector(); if (customSelector) { try { const customElements = document.querySelectorAll(customSelector); if (customElements.length > 0) { console.log(`[论坛字号放大器] 命中自定义规则 (${window.location.hostname}): ${customSelector}`); return Array.from(customElements); } } catch (e) { console.error('[论坛字号放大器] 自定义选择器语法错误:', e); } } // 策略 A:匹配内置预设规则 for (const selector of COMMON_SELECTORS) { const elements = document.querySelectorAll(selector); if (elements.length > 0) { console.log(`[论坛字号放大器] 命中预设规则: ${selector}`); return Array.from(elements); } } // 策略 B:启发式降级分析 console.log('[论坛字号放大器] 启动启发式分析...'); const candidates = Array.from(document.querySelectorAll('div, article, td')); return candidates.filter(el => { const textLength = el.textContent.trim().length; const childContainers = el.querySelectorAll('div, section, article'); return textLength > 120 && childContainers.length < 3; }); } /** * 安全放大字号(双层配置联动,防止死循环叠加与粗体渲染异常) * @param {Element[]} elements 目标节点数组 */ function applySafeEnlargement(elements) { if (!elements || elements.length === 0) return; // 计算当前生效的增量(域名优先,全局保底) const activeDelta = getEffectiveFontDelta(); elements.forEach(el => { // 1. 记录基准字号(防无限叠加) if (!el.dataset.originalFontSize) { const computedStyle = window.getComputedStyle(el); const baseSize = parseFloat(computedStyle.fontSize); if (!isNaN(baseSize) && baseSize > 0) { el.dataset.originalFontSize = baseSize; } } const baseFontSize = parseFloat(el.dataset.originalFontSize); if (!isNaN(baseFontSize) && baseFontSize > 0) { const targetFontSize = baseFontSize + activeDelta; // 应用字号与相对行高 el.style.setProperty('font-size', `${targetFontSize}px`, 'important'); el.style.setProperty('line-height', DEFAULT_CONFIG.lineHeight, 'important'); // 强制锁定字重并开启抗锯齿,修复部分论坛变粗问题 el.style.setProperty('font-weight', '400', 'important'); el.style.setProperty('-webkit-font-smoothing', 'antialiased', 'important'); // 解除容器限制,防止溢出截断 const computedStyle = window.getComputedStyle(el); if (computedStyle.maxHeight !== 'none') { el.style.setProperty('max-height', 'none', 'important'); } if (computedStyle.overflow === 'hidden') { el.style.setProperty('overflow', 'visible', 'important'); } el.dataset.fontEnlarged = 'true'; } }); } /** * 生成泛化 CSS 选择器(忽略动态数字 ID,覆盖所有楼层) * @param {Element} el * @returns {string} */ function generateSelector(el) { const isGenericClass = (cls) => { return cls && typeof cls === 'string' && !/\d{3,}/.test(cls) && !cls.startsWith('js-') && !cls.includes(':'); }; if (el.className && typeof el.className === 'string') { const validClasses = el.className.split(/\s+/).filter(isGenericClass); if (validClasses.length > 0) { return `.${validClasses.join('.')}`; } } let current = el; let depth = 0; while (current && current !== document.body && depth < 3) { if (current.className && typeof current.className === 'string') { const classes = current.className.split(/\s+/).filter(isGenericClass); if (classes.length > 0) { return `.${classes.join('.')} ${el.tagName.toLowerCase()}`; } } current = current.parentElement; depth++; } return el.tagName.toLowerCase(); } /** * 可视化拾取模式 */ function startVisualPicker() { let hoveredEl = null; const styleEl = document.createElement('style'); styleEl.id = 'font-enlarger-picker-style'; styleEl.textContent = ` .fe-picker-highlight { outline: 2px dashed #ff4757 !important; outline-offset: -2px !important; background-color: rgba(255, 71, 87, 0.08) !important; cursor: crosshair !important; } `; document.head.appendChild(styleEl); const onMouseOver = (e) => { e.stopPropagation(); if (hoveredEl) hoveredEl.classList.remove('fe-picker-highlight'); hoveredEl = e.target; hoveredEl.classList.add('fe-picker-highlight'); }; const onClick = (e) => { e.preventDefault(); e.stopPropagation(); cleanup(); if (hoveredEl) { const selector = generateSelector(hoveredEl); const confirmed = confirm(`已选中目标区域,生成的通用选择器为:\n\n${selector}\n\n是否绑定到当前域名 (${window.location.hostname})?`); if (confirmed) { GM_setValue(`${CONFIG_KEYS.DOMAIN_SELECTOR_PREFIX}${window.location.hostname}`, selector); alert('配置已保存!页面即将刷新并应用规则。'); window.location.reload(); } } }; const onKeyDown = (e) => { if (e.key === 'Escape') { cleanup(); alert('已退出可视化选取模式。'); } }; function cleanup() { if (hoveredEl) hoveredEl.classList.remove('fe-picker-highlight'); document.removeEventListener('mouseover', onMouseOver, true); document.removeEventListener('click', onClick, true); document.removeEventListener('keydown', onKeyDown, true); const style = document.getElementById('font-enlarger-picker-style'); if (style) style.remove(); } document.addEventListener('mouseover', onMouseOver, true); document.addEventListener('click', onClick, true); document.addEventListener('keydown', onKeyDown, true); alert('【可视化元素拾取模式已激活】\n\n请将鼠标悬停在需要放大的论坛内容上(有红框高亮),然后【单击鼠标左键】完成选择。\n按 ESC 键退出。'); } /** * 主初始化逻辑 */ function init() { // 【新增】优先校验黑名单:若当前网站已设置禁用,则直接中断退出 if (isDomainDisabled()) { console.log(`[论坛字号放大器] 当前站点 (${window.location.hostname}) 已设为禁用,跳过字号处理。`); return; } const targetNodes = findContentNodes(); if (targetNodes.length > 0) { applySafeEnlargement(targetNodes); console.log(`[论坛字号放大器] 成功处理 ${targetNodes.length} 个节点,当前生效字号增量: +${getEffectiveFontDelta()}px`); } else { console.warn('[论坛字号放大器] 未能自动识别到符合条件的内容区域。'); } } // 1. 运行初始化 init(); // 2. DOM 防抖变动监听(仅监听 DOM 树插入,忽略属性改变) let observerTimer = null; const observer = new MutationObserver((mutations) => { const hasNewNodes = mutations.some(m => m.addedNodes.length > 0); if (!hasNewNodes) return; if (observerTimer) clearTimeout(observerTimer); observerTimer = setTimeout(() => { init(); }, 500); }); observer.observe(document.body, { childList: true, subtree: true, attributes: false }); // 3. 注册插件菜单管理面板 const domainDelta = getDomainFontDelta(); const effectiveDelta = getEffectiveFontDelta(); const domainStatusText = domainDelta !== null ? `+${domainDelta}px (独立设置)` : `+${getGlobalFontDelta()}px (继承通用配置)`; // 菜单 1:设置全局通用放大倍率 GM_registerMenuCommand(`🌐 设置全局通用字号增量 (当前: +${getGlobalFontDelta()}px)`, () => { const input = prompt( `[论坛字号放大器 - 全局配置]\n\n设置未单独配置过论坛的默认放大字号 (单位: px):\n当前全局设置: +${getGlobalFontDelta()}px`, getGlobalFontDelta() ); if (input !== null) { const val = parseInt(input.trim(), 10); if (!isNaN(val) && val > 0) { GM_setValue(CONFIG_KEYS.GLOBAL_DELTA, val); alert(`全局设置成功!通用字号增量已调整为 +${val}px,页面即将刷新。`); window.location.reload(); } else { alert('请输入有效的正整数!'); } } }); // 菜单 2:设置当前网站独立倍率 GM_registerMenuCommand(`🏠 设置当前网站放大增量 (当前: ${domainStatusText})`, () => { const currentVal = domainDelta !== null ? domainDelta : getGlobalFontDelta(); const input = prompt( `[论坛字号放大器 - 站点配置]\n当前域名: ${window.location.hostname}\n\n请输入在此论坛专属的放大字号 (单位: px):\n(若要恢复继承全局配置,请输入 0 或留空)`, currentVal ); if (input !== null) { const trimmed = input.trim(); if (trimmed === '' || trimmed === '0') { // 清除独立设置,恢复继承全局 GM_setValue(`${CONFIG_KEYS.DOMAIN_DELTA_PREFIX}${window.location.hostname}`, null); alert(`已重置当前网站配置,恢复继承全局设置 (+${getGlobalFontDelta()}px)!`); window.location.reload(); } else { const val = parseInt(trimmed, 10); if (!isNaN(val) && val > 0) { GM_setValue(`${CONFIG_KEYS.DOMAIN_DELTA_PREFIX}${window.location.hostname}`, val); alert(`绑定成功!域名 ${window.location.hostname} 专属字号增量已设置为 +${val}px。`); window.location.reload(); } else { alert('请输入有效的正整数(或输入 0 恢复全局设置)!'); } } } }); // 菜单 3:可视化拾取元素 GM_registerMenuCommand('🎯 开启可视化点击拾取区域', startVisualPicker); // 菜单 4:重置当前站点所有规则(选择器 + 专属增量 + 禁用状态) GM_registerMenuCommand('🗑️ 重置当前网站的所有自定义规则', () => { const selector = getDomainSelector(); const delta = getDomainFontDelta(); const disabled = isDomainDisabled(); // 【新增】读取禁用状态 if (!selector && delta === null && !disabled) { // 【修改】将 disabled 加入判空条件 alert('当前网站没有任何专属自定义设置。'); return; } if (confirm(`确定要重置域名 ${window.location.hostname} 的所有专属配置吗?\n\n已保存的选择器: ${selector || '无'}\n已保存的增量: ${delta !== null ? '+' + delta + 'px' : '无'}\n禁用状态: ${disabled ? '已禁用' : '正常启用'}`)) { GM_setValue(`${CONFIG_KEYS.DOMAIN_SELECTOR_PREFIX}${window.location.hostname}`, ''); GM_setValue(`${CONFIG_KEYS.DOMAIN_DELTA_PREFIX}${window.location.hostname}`, null); setDomainDisabled(false); // 【新增】一键重置时同时恢复启用状态 alert('当前站点配置已彻底清空并重新启用,页面即将刷新。'); window.location.reload(); } }); // ========================================== // 【新增】菜单项:切换当前网站的启用/禁用状态 // ========================================== const isDisabled = isDomainDisabled(); const switchMenuText = isDisabled ? `🟢 开启当前网站放大功能 (当前: 已禁用)` : `🚫 在当前网站禁用此插件 (当前: 已启用)`; GM_registerMenuCommand(switchMenuText, () => { if (isDisabled) { setDomainDisabled(false); alert(`已在当前网站 (${window.location.hostname}) 重新启用放大功能!`); } else { setDomainDisabled(true); alert(`已在当前网站 (${window.location.hostname}) 禁用放大功能!页面将恢复原生样式。`); } window.location.reload(); }); })();