// ==UserScript== // @name NiceFont (耐视字体) // @name:zh-CN NiceFont (耐视字体) // @name:zh-TW NiceFont(耐視字體) // @name:en NiceFont // @name:ko NiceFont (좋은 글꼴) // @name:ja NiceFont (いいフォント) // @name:ru NiceFont (Хороший шрифт) // @name:fr NiceFont (Police agréable) // @name:de NiceFont (Schöne Schrift) // @name:es NiceFont (Fuente agradable) // @name:pt NiceFont (Fonte agradável) // @version 3.1 // @author DD1024z // @description NiceFont: 是一款优化网页字体显示的强大工具,让浏览更清晰、舒适!“真正调整字体,而非页面缩放,拒绝将就”!可直接修改网页的字体大小与风格,保存你的字体设置,轻松应用到每个网页,支持首次、定时或动态调整字体,适配子域名、整站或全局设置,兼容B站评论区、钉钉文档、知乎、论坛等几乎所有网站! // @description:zh-CN NiceFont: 是一款优化网页字体显示的强大工具,让浏览更清晰、舒适!“真正调整字体,而非页面缩放,拒绝将就”!可直接修改网页的字体大小与风格,保存你的字体设置,轻松应用到每个网页,支持首次、定时或动态调整字体,适配子域名、整站或全局设置,兼容B站评论区、钉钉文档、知乎、论坛等几乎所有网站! // @description:zh-TW NiceFont:是一款優化網頁字體顯示的強大工具,讓瀏覽更清晰、舒適!「真正調整字體,而非頁面縮放,拒絕將就」!可直接修改網頁的字體大小與風格,儲存你的字體設定,輕鬆應用到每個網頁,支援首次、定時或動態調整字體,適配子域名、整站或全局設定,相容B站評論區、釘釘文件、知乎、論壇等幾乎所有網站! // @description:en NiceFont: A powerful tool to optimize web font display for clearer, more comfortable browsing! "Truly adjusts fonts, not page scaling—settle for nothing less!" Directly modifies font size and style, saves your settings, and applies them effortlessly to every page. Supports one-time, scheduled, or dynamic font adjustments, adaptable to subdomains, entire sites, or global settings. Compatible with nearly all websites, including Bilibili comments, DingTalk docs, Zhihu, forums, and more! // @description:ko NiceFont: 웹 폰트 표시를 최적화하여 더 선명하고 편안한 브라우징을 제공하는 강력한 도구! "페이지를 스케일링하지 않고 진정으로 폰트를 조정—타협하지 마세요!" 폰트 크기와 스타일을 직접 수정하고, 설정을 저장하여 모든 페이지에 쉽게 적용합니다. 최초, 정기 또는 동적 폰트 조정을 지원하며, 서브도메인, 전체 사이트 또는 전역 설정에 적응 가능. Bilibili 댓글, DingTalk 문서, Zhihu, 포럼 등 거의 모든 웹사이트와 호환! // @description:ja NiceFont:ウェブフォント表示を最適化し、よりクリアで快適な閲覧を実現する強力なツール!「ページのスケーリングではなく、フォントを本当の意味で調整—妥協はなし!」フォントサイズとスタイルを直接変更し、設定を保存してすべてのページに簡単に適用。初回、定期、または動的フォント調整をサポートし、サブドメイン、サイト全体、またはグローバル設定に適応可能。ビリビリコメント、DingTalkドキュメント、Zhihu、フォーラムなど、ほぼすべてのウェブサイトに対応! // @description:ru NiceFont: Мощный инструмент для оптимизации отображения веб-шрифтов, обеспечивающий более четкое и комфортное чтение! "По-настоящему регулирует шрифты, а не масштабирует страницу — никаких компромиссов!" Позволяет напрямую изменять размер и стиль шрифта, сохранять настройки и легко применять их к каждой странице. Поддерживает разовую, запланированную или динамическую настройку шрифтов, адаптируется к поддоменам, целым сайтам или глобальным настройкам. Совместим с большинством сайтов, включая комментарии на Bilibili, документы DingTalk, Zhihu, форумы и другие! // @description:fr NiceFont : Un outil puissant pour optimiser l'affichage des polices web, rendant la navigation plus claire et confortable ! « Ajuste réellement les polices, pas un simple zoom de page — refusez les compromis ! » Modifie directement la taille et le style des polices, enregistre vos paramètres et les applique facilement à chaque page. Prend en charge les ajustements uniques, programmés ou dynamiques des polices, adaptable aux sous-domaines, sites entiers ou paramètres globaux. Compatible avec presque tous les sites, y compris les commentaires de Bilibili, les documents DingTalk, Zhihu, les forums, et plus encore ! // @description:de NiceFont: Ein leistungsstarkes Tool zur Optimierung der Webschriftanzeige für klareres und angenehmeres Surfen! "Passt Schriften wirklich an, statt die Seite zu skalieren — keine Kompromisse!" Ändert Schriftgröße und -stil direkt, speichert Ihre Einstellungen und wendet sie mühelos auf jede Seite an. Unterstützt einmalige, geplante oder dynamische Schrifteinstellungen, anpassbar an Subdomains, ganze Websites oder globale Einstellungen. Kompatibel mit fast allen Websites, einschließlich Bilibili-Kommentaren, DingTalk-Dokumenten, Zhihu, Foren und mehr! // @description:es NiceFont: ¡Una poderosa herramienta para optimizar la visualización de fuentes web, haciendo que la navegación sea más clara y cómoda! "Ajusta realmente las fuentes, no solo escala la página — ¡sin concesiones!" Modifica directamente el tamaño y estilo de la fuente, guarda tus configuraciones y las aplica fácilmente a cada página. Admite ajustes únicos, programados o dinámicos de fuentes, adaptable a subdominios, sitios completos o configuraciones globales. Compatible con casi todos los sitios web, incluidos comentarios de Bilibili, documentos de DingTalk, Zhihu, foros y más! // @description:pt NiceFont: Uma ferramenta poderosa para otimizar a exibição de fontes na web, tornando a navegação mais clara e confortável! "Ajusta realmente as fontes, não apenas escala a página — sem concessões!" Modifica diretamente o tamanho e estilo da fonte, salva suas configurações e as aplica facilmente a cada página. Suporta ajustes únicos, agendados ou dinâmicos de fontes, adaptável a subdomínios, sites inteiros ou configurações globais. Compatível com quase todos os sites, incluindo comentários do Bilibili, documentos do DingTalk, Zhihu, fóruns e mais! // @homepageURL https://github.com/10D24D/NiceFont/ // @namespace https://github.com/10D24D/NiceFont/ // @icon https://raw.githubusercontent.com/10D24D/NiceFont/main/static/logo.png // @match *://*/* // @license Apache License 2.0 // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_info // @run-at document-start // @compatible edge version≥90 (Compatible Tampermonkey, Violentmonkey) // @compatible Chrome version≥90 (Compatible Tampermonkey, Violentmonkey) // @compatible Firefox version≥84 (Compatible Greasemonkey, Tampermonkey, Violentmonkey) // @compatible Opera version≥78 (Compatible Tampermonkey, Violentmonkey) // @compatible Safari version≥15.4 (Compatible Tampermonkey, Userscripts) // @create 2025-4-18 // @copyright 2025, DD1024z // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 调试开关,生产环境中禁用日志 const enableLogging = true; // 关闭跟踪常量 const CLOSE_TRACKING_WINDOW = 1800 * 1000; // 30 分钟(毫秒) const CLOSE_COUNT_THRESHOLD = 2; // 连续关闭两次 /** * 自定义日志函数,仅在调试模式下输出 * @param {...any} args - 日志参数 */ function log(...args) { if (enableLogging) { console.log('[NiceFont]', ...args); } } // 跳过 iframe 执行 if (window.top !== window.self) { log('跳过 iframe 执行'); return; } // --- 工具函数模块 --- const Utils = { /** * 节流函数,限制函数调用频率 * @param {Function} fn - 要节流的函数 * @param {number} wait - 节流间隔(毫秒) * @returns {Function} 节流后的函数 */ throttle(fn, wait) { let lastCall = 0; return function (...args) { const now = Date.now(); if (now - lastCall >= wait) { lastCall = now; fn(...args); } }; }, /** * 将字体大小单位转换为像素 * @param {HTMLElement} el - 元素 * @param {string} fontSize - 字体大小(带单位) * @returns {number} 像素值 */ convertToPx(el, fontSize) { if (!fontSize) return 16; if (fontSize.includes('rem')) { const rootFontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize); return parseFloat(fontSize) * rootFontSize; } if (fontSize.includes('em')) { const parentFontSize = parseFloat(window.getComputedStyle(el.parentElement).fontSize); return parseFloat(fontSize) * parentFontSize; } if (fontSize.includes('%')) { const parentFontSize = parseFloat(window.getComputedStyle(el.parentElement).fontSize); return (parseFloat(fontSize) / 100) * parentFontSize; } if (fontSize.includes('pt')) { return parseFloat(fontSize) * 1.3333; } if (fontSize.includes('vw')) { return parseFloat(fontSize) * window.innerWidth / 100; } if (fontSize.includes('vh')) { return parseFloat(fontSize) * window.innerHeight / 100; } return parseFloat(fontSize); }, /** * 检查元素是否包含可见文本 * @param {HTMLElement} el - 元素 * @returns {boolean} 是否包含可见文本 */ hasVisibleText(el) { return Array.from(el.childNodes).some( node => node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '' ); }, /** * 获取顶级域名 * @returns {string} 顶级域名(如 .example.com) */ getTopLevelDomain() { const hostname = window.location.hostname; const parts = hostname.split('.'); return parts.length >= 2 ? `.${parts.slice(-2).join('.')}` : hostname; } }; // --- 状态管理 --- const State = { fontIncrement: 1, currentFontFamily: 'none', currentAdjustment: 0, watchDOMChanges: false, intervalSeconds: 0, firstAdjustment: false, firstAdjustmentTime: 3, currentLanguage: 'en', panelType: 'floating', isConfigModified: false, targetScope: 1, pendingScopeChange: null, observer: null, timer: null, /** * 获取状态值 * @param {string} key - 状态键 * @returns {any} 状态值 */ get(key) { return this[key]; }, /** * 设置状态值 * @param {string} key - 状态键 * @param {any} value - 状态值 */ set(key, value) { this[key] = value; } }; // --- 配置范围管理 --- const ConfigScopeManager = { BASE_STORAGE_KEY: 'NiceFont_config', GLOBAL_DEFAULT_KEY: 'NiceFont_global_default_config', PANEL_TYPE_KEY: 'NiceFont_panelType', scopeMap: { 1: 'subdomain', 2: 'topLevelDomain', 3: 'allWebsites' }, /** * 初始化动态键 */ initKeys() { this.subdomainKey = `${this.BASE_STORAGE_KEY}_${window.location.hostname}`; this.topLevelKey = `${this.BASE_STORAGE_KEY}_${Utils.getTopLevelDomain()}`; }, /** * 获取当前配置键 * @returns {string} 配置键 */ getConfigKey() { this.initKeys(); const scope = State.get('targetScope'); if (scope === 1) return this.subdomainKey; if (scope === 2) return this.topLevelKey; return this.GLOBAL_DEFAULT_KEY; }, /** * 获取当前生效的配置范围 * @returns {number} 范围(1: 子域名, 2: 顶级域名, 3: 所有网站) */ getEffectiveScope() { this.initKeys(); const subdomainConfig = GM_getValue(this.subdomainKey, {}); const topLevelConfig = GM_getValue(this.topLevelKey, {}); const globalConfig = GM_getValue(this.GLOBAL_DEFAULT_KEY, {}); if (Object.keys(subdomainConfig).length > 0) return 1; if (Object.keys(topLevelConfig).length > 0) return 2; if (Object.keys(globalConfig).length > 0) return 3; return 1; // 默认返回子域名 }, /** * 检查当前网站是否已有配置 * @returns {boolean} 是否存在配置 */ hasConfig() { this.initKeys(); const configKey = this.getConfigKey(); const config = GM_getValue(configKey, null); const hasConfig = !!config && Object.keys(config).length > 0; log(`检查配置: key=${configKey}, hasConfig=${hasConfig}, config=${JSON.stringify(config)}`); return hasConfig; }, /** * 获取范围显示文本 * @param {number} scope - 范围 * @param {Object} t - 翻译对象 * @returns {string} 显示文本 */ getScopeText(scope, t) { return scope === 1 ? t.subdomain : scope === 2 ? t.topLevelDomain : t.allWebsites; }, /** * 获取当前配置来源文本 * @param {Object} t - 翻译对象 * @returns {string} 配置来源文本 */ getCurrentConfigText(t) { this.initKeys(); const subdomainConfig = GM_getValue(this.subdomainKey, {}); const topLevelConfig = GM_getValue(this.topLevelKey, {}); const globalConfig = GM_getValue(this.GLOBAL_DEFAULT_KEY, {}); if (Object.keys(subdomainConfig).length > 0) return window.location.hostname; if (Object.keys(topLevelConfig).length > 0) return `*.${Utils.getTopLevelDomain().replace(/^\./, '')}`; if (Object.keys(globalConfig).length > 0) return t.allWebsites; return t.notConfigured; }, /** * 获取配置范围显示文本(包含目标范围) * @param {Object} t - 翻译对象 * @returns {string} 显示文本 */ getConfigScopeDisplayText(t) { const effectiveScope = this.getEffectiveScope(); const currentScopeText = this.getScopeText(effectiveScope, t); const pendingScope = State.get('pendingScopeChange'); if (pendingScope && pendingScope !== effectiveScope) { const targetScopeText = this.getScopeText(pendingScope, t); return `${currentScopeText} -> ${targetScopeText}`; } return currentScopeText; }, /** * 删除指定范围的配置 * @param {number} scope - 范围 * @returns {boolean} 是否删除成功 */ deleteConfig(scope) { this.initKeys(); const t = translations[State.get('currentLanguage')] || translations.en; let key, target; if (scope === 1) { key = this.subdomainKey; target = window.location.hostname; } else if (scope === 2) { key = this.topLevelKey; target = `*.${Utils.getTopLevelDomain().replace(/^\./, '')}`; } else { key = this.GLOBAL_DEFAULT_KEY; target = t.allWebsites; } GM_setValue(key, {}); log(`删除配置: ${target}`); return true; } }; // --- 配置管理 --- const ConfigManager = { /** * 加载配置 */ loadConfig() { ConfigScopeManager.initKeys(); let config = GM_getValue(ConfigScopeManager.subdomainKey, {}); let effectiveScope = 1; if (Object.keys(config).length === 0) { config = GM_getValue(ConfigScopeManager.topLevelKey, {}); effectiveScope = 2; if (Object.keys(config).length === 0) { config = GM_getValue(ConfigScopeManager.GLOBAL_DEFAULT_KEY, {}); effectiveScope = Object.keys(config).length > 0 ? 3 : 1; // 空全局配置时默认子域名 } } State.set('fontIncrement', config.increment || 1); State.set('currentFontFamily', config.fontFamily || 'none'); State.set('currentAdjustment', config.resize || 0); State.set('watchDOMChanges', config.watcher || false); State.set('intervalSeconds', config.timer || 0); State.set('firstAdjustment', config.first || false); State.set('firstAdjustmentTime', config.firstTime || 3); State.set('targetScope', effectiveScope); log('加载配置:', config, '生效范围:', effectiveScope); }, /** * 保存配置 */ saveConfig() { const t = translations[State.get('currentLanguage')] || translations.en; // 使用 pendingScopeChange(若存在),否则使用 targetScope let scope = State.get('pendingScopeChange') !== null ? State.get('pendingScopeChange') : State.get('targetScope'); // 如果配置已修改且无 pendingScopeChange,优先使用 UI 显示的 scope if (State.get('isConfigModified') && State.get('pendingScopeChange') === null) { scope = ConfigScopeManager.getEffectiveScope(); if (scope === 3 && Object.keys(GM_getValue(ConfigScopeManager.GLOBAL_DEFAULT_KEY, {})).length === 0) { scope = 1; // 无全局配置时,默认子域名 } } const scopeText = ConfigScopeManager.getScopeText(scope, t); const target = scope === 1 ? window.location.hostname : scope === 2 ? `*.${Utils.getTopLevelDomain().replace(/^\./, '')}` : t.allWebsites; const confirmMessage = scope === 3 ? t.saveConfigConfirm.replace('{scope}', scopeText).replace(' [{target}]', '') : t.saveConfigConfirm.replace('{scope}', scopeText).replace('{target}', target); if (confirm(confirmMessage)) { const config = { increment: State.get('fontIncrement'), resize: State.get('currentAdjustment'), watcher: State.get('watchDOMChanges'), timer: State.get('intervalSeconds'), fontFamily: State.get('currentFontFamily'), first: State.get('firstAdjustment'), firstTime: State.get('firstAdjustmentTime') }; ConfigScopeManager.initKeys(); const key = scope === 1 ? ConfigScopeManager.subdomainKey : scope === 2 ? ConfigScopeManager.topLevelKey : ConfigScopeManager.GLOBAL_DEFAULT_KEY; GM_setValue(key, config); State.set('isConfigModified', false); State.set('targetScope', scope); State.set('pendingScopeChange', null); ConfigManager.loadConfig(); // 刷新配置 UIManager.updateUI(); log(`保存配置到: ${target} (scope=${scope})`); } }, /** * 更改配置范围 */ changeConfigScope() { const t = translations[State.get('currentLanguage')] || translations.en; const effectiveScope = ConfigScopeManager.getEffectiveScope(); const currentScopeText = ConfigScopeManager.getScopeText(effectiveScope, t); const input = prompt( t.configScopePrompt .replace('{scope}', currentScopeText) .replace('{hostname}', window.location.hostname) .replace('{tld}', Utils.getTopLevelDomain().replace(/^\./, '')), State.get('targetScope') ); const newScope = parseInt(input, 10); if (![1, 2, 3].includes(newScope)) { if (input !== null) alert(t.invalidInput); return; } if (newScope === effectiveScope) { log(`新范围与当前范围相同: ${ConfigScopeManager.scopeMap[newScope]}`); return; } ConfigScopeManager.initKeys(); const hasConfig = effectiveScope === 1 ? Object.keys(GM_getValue(ConfigScopeManager.subdomainKey, {})).length > 0 : effectiveScope === 2 ? Object.keys(GM_getValue(ConfigScopeManager.topLevelKey, {})).length > 0 : Object.keys(GM_getValue(ConfigScopeManager.GLOBAL_DEFAULT_KEY, {})).length > 0; if (newScope > effectiveScope && hasConfig) { const confirmMessage = effectiveScope === 3 ? `${t.currentConfigScope}: ${ConfigScopeManager.getCurrentConfigText(t)}\n${t.deleteBeforeScopeChangeConfirm.replace('{scope}', ConfigScopeManager.getScopeText(effectiveScope, t)).replace(' [{target}]', '')}` : `${t.currentConfigScope}: ${ConfigScopeManager.getCurrentConfigText(t)}\n${t.deleteBeforeScopeChangeConfirm.replace('{scope}', ConfigScopeManager.getScopeText(effectiveScope, t)).replace('{target}', ConfigScopeManager.getCurrentConfigText(t))}`; if (confirm(confirmMessage)) { ConfigScopeManager.deleteConfig(effectiveScope); State.set('pendingScopeChange', newScope); State.set('targetScope', newScope); State.set('isConfigModified', true); UIManager.updateUI(); log(`标记范围更改为: ${ConfigScopeManager.scopeMap[newScope]}`); } } else { State.set('pendingScopeChange', newScope); State.set('targetScope', newScope); State.set('isConfigModified', true); UIManager.updateUI(); log(`标记范围更改为: ${ConfigScopeManager.scopeMap[newScope]}`); } }, /** * 删除当前配置 */ deleteCurrentConfig() { const effectiveScope = ConfigScopeManager.getEffectiveScope(); const t = translations[State.get('currentLanguage')] || translations.en; const scopeText = ConfigScopeManager.getScopeText(effectiveScope, t); const target = ConfigScopeManager.getCurrentConfigText(t); if (target === t.notConfigured) { log('无配置可删除'); return false; } const confirmMessage = effectiveScope === 3 ? `${t.currentConfigScope}: ${target}\n${t.deleteConfigConfirm.replace('{scope}', scopeText).replace(' [{target}]', '')}` : `${t.currentConfigScope}: ${target}\n${t.deleteConfigConfirm.replace('{scope}', scopeText).replace('{target}', target)}`; if (confirm(confirmMessage)) { ConfigScopeManager.deleteConfig(effectiveScope); State.set('targetScope', 1); // 强制设为子域名 State.set('pendingScopeChange', null); // 清空待定范围 ConfigManager.loadConfig(); UIManager.updateUI(); log('配置已删除,targetScope 重置为 1'); return true; } return false; } }; // --- 字体管理 --- const FontManager = { supportFonts: [ 'custom', 'auto', 'Arial', 'cursive', 'fangsong', 'fantasy', 'monospace', 'none', 'sans-serif', 'serif', 'system-ui', 'ui-monospace', 'ui-rounded', 'ui-sans-serif', 'ui-serif', '-webkit-body', 'inherit', 'initial', 'unset', 'Verdana', 'Helvetica', 'Tahoma', 'Times New Roman', 'Georgia', 'Courier New', 'Comic Sans MS' ], styleCache: new WeakMap(), /** * 获取缓存的计算样式 * @param {HTMLElement} el - 元素 * @returns {CSSStyleDeclaration} 计算样式 */ getCachedStyle(el) { if (!this.styleCache.has(el)) { this.styleCache.set(el, window.getComputedStyle(el)); } return this.styleCache.get(el); }, /** * 递归遍历 DOM 元素 * @param {HTMLElement} el - 根元素 * @param {Function} callback - 回调函数 */ traverseDOM(el, callback) { if (el.nodeType !== Node.ELEMENT_NODE || el.id === 'NiceFont_panel' || el.hasAttribute('data-nicefont-panel')) { return; } callback(el); if (el.tagName === 'IFRAME') { try { const iframeDoc = el.contentDocument || el.contentWindow.document; if (iframeDoc) { this.traverseDOM(iframeDoc.body, callback); const font = State.get('currentFontFamily'); if (font !== 'none') { iframeDoc.documentElement.style.setProperty('--nicefont-family', font); } else { iframeDoc.documentElement.style.removeProperty('--nicefont-family'); } } } catch (e) { console.error('[NiceFont] 访问 iframe 失败:', e); } } if (el.shadowRoot) { try { el.shadowRoot.querySelectorAll('*').forEach(child => this.traverseDOM(child, callback)); } catch (e) { console.error('[NiceFont] 处理 Shadow DOM 失败:', e); } } Array.from(el.children).forEach(child => requestAnimationFrame(() => this.traverseDOM(child, callback))); }, /** * 应用字体调整 * @param {HTMLElement} el - 根元素 * @param {number} increment - 字体大小增量(px) */ applyFontRecursively(el, increment) { this.traverseDOM(el, (node) => { const style = this.getCachedStyle(node); const isVisible = style.display !== 'none' && style.visibility !== 'hidden'; if (Utils.hasVisibleText(node) && isVisible) { let currentFontSize = node.style.fontSize || style.fontSize; if (!node.hasAttribute('data-default-fontsize')) { node.setAttribute('data-default-fontsize', currentFontSize); } const baseFontSize = parseFloat(Utils.convertToPx(node, node.getAttribute('data-default-fontsize'))); if (!isNaN(baseFontSize)) { node.style.fontSize = `${baseFontSize + increment}px`; } } }); const font = State.get('currentFontFamily'); if (font !== 'none') { document.documentElement.style.setProperty('--nicefont-family', font); } else { document.documentElement.style.removeProperty('--nicefont-family'); } }, /** * 重置字体 * @param {HTMLElement} el - 根元素 */ resetFont(el) { this.traverseDOM(el, (node) => { const defaultSize = node.getAttribute('data-default-fontsize'); if (defaultSize) { node.style.fontSize = defaultSize; node.removeAttribute('data-default-fontsize'); } else { node.style.removeProperty('font-size'); } node.style.removeProperty('font-family'); }); try { document.documentElement.style.removeProperty('--nicefont-family'); } catch (e) { console.error('[NiceFont] 移除 --nicefont-family 失败:', e); } // 重置关闭跟踪状态 GM_setValue('NiceFont_closeCount', 0); GM_setValue('NiceFont_lastCloseTime', 0); GM_setValue('NiceFont_autoOpenDisabled', false); log('重置关闭跟踪状态'); }, /** * 修改字体大小 * @param {number} increment - 增量(px) */ changeFontSize(increment) { State.set('currentAdjustment', State.get('currentAdjustment') + increment); this.applyFontRecursively(document.body, State.get('currentAdjustment')); State.set('isConfigModified', true); UIManager.updateUI(); log(`字体大小调整: ${increment}px, 当前: ${State.get('currentAdjustment')}px`); } }; // --- 界面管理 --- const UIManager = { menuHandles: [], panelCache: null, overlayCache: null, lastToggleTime: 0, // 用于防抖 /** * 定义命令配置 * @returns {Array} 命令配置数组 */ getCommandsConfig() { const t = translations[State.get('currentLanguage')] || translations.en; return [ { id: 'setFontFamily', getText: () => `🔠 ${t.setFontFamily}: ${State.get('currentFontFamily')}`, action: () => { const t = translations[State.get('currentLanguage')] || translations.en; let select = document.getElementById('NiceFont_font-family'); if (select) { select.remove(); document.removeEventListener('click', this.closeDropdown); return; } select = document.createElement('select'); select.id = 'NiceFont_font-family'; select.className = 'font-family-select'; select.innerHTML = FontManager.supportFonts.map(font => `` ).join(''); const btn = document.getElementById('NiceFont_setFontFamily'); if (btn) btn.appendChild(select); select.focus(); select.addEventListener('click', e => e.stopPropagation()); select.addEventListener('change', (e) => { const selectedFont = e.target.value; if (selectedFont === 'custom') { const input = prompt(`${t.setFontFamilyPrompt}\n\n${t.supportFontFamily}\n${FontManager.supportFonts.slice(0, -1).join(', ')}`, ''); if (input && input.trim()) { const newFont = input.trim(); if (!FontManager.supportFonts.includes(newFont)) { FontManager.supportFonts.splice(FontManager.supportFonts.length - 1, 0, newFont); const option = document.createElement('option'); option.value = newFont; option.textContent = newFont; select.insertBefore(option, select.lastChild); } State.set('currentFontFamily', newFont); select.value = newFont; } else { select.value = State.get('currentFontFamily'); select.remove(); document.removeEventListener('click', this.closeDropdown); log('取消自定义字体输入'); return; } } else { State.set('currentFontFamily', selectedFont); } FontManager.applyFontRecursively(document.body, State.get('currentAdjustment')); State.set('isConfigModified', true); UIManager.updateUI(); select.remove(); document.removeEventListener('click', this.closeDropdown); log(`字体类型设置为: ${State.get('currentFontFamily')}`); }); this.closeDropdown = (event) => { if (!select.contains(event.target) && !btn.contains(event.target)) { select.remove(); document.removeEventListener('click', this.closeDropdown); log('下拉菜单关闭'); } }; document.addEventListener('click', this.closeDropdown); } }, { id: 'status', getText: () => `📏 ${t.fontSizeAdjustment}: ${State.get('currentAdjustment') >= 0 ? '+' : ''}${State.get('currentAdjustment')}px`, action: () => { } }, { id: 'increase', getText: () => `🔼 ${t.increase}`, action: () => FontManager.changeFontSize(State.get('fontIncrement')), autoClose: false }, { id: 'decrease', getText: () => `🔽 ${t.decrease}`, action: () => FontManager.changeFontSize(-State.get('fontIncrement')), autoClose: false }, { id: 'reset', getText: () => `🔄️ ${t.reset}`, action: () => { FontManager.resetFont(document.body); State.set('currentAdjustment', 0); State.set('currentFontFamily', 'none'); State.set('watchDOMChanges', false); State.set('intervalSeconds', 0); State.set('firstAdjustment', false); State.set('firstAdjustmentTime', 3); if (State.get('observer')) { State.get('observer').disconnect(); State.set('observer', null); } if (State.get('timer')) { clearInterval(State.get('timer')); State.set('timer', null); } State.set('isConfigModified', true); UIManager.updateUI(); log('字体设置重置'); } }, { id: 'first-adjustment', getText: () => `1️⃣ ${State.get('firstAdjustment') ? t.firstAdjustmentEnabled : t.firstAdjustmentDisabled} ${State.get('firstAdjustment') ? `【${State.get('firstAdjustmentTime')}s】` : ''}`, action: () => { const input = prompt(t.firstAdjustmentConfirm, State.get('firstAdjustmentTime').toString()); const secs = parseInt(input, 10); if (!isNaN(secs)) { State.set('firstAdjustment', !State.get('firstAdjustment')); State.set('firstAdjustmentTime', secs); if (secs === 0) State.set('firstAdjustment', false); if (State.get('firstAdjustment')) { setTimeout(() => { FontManager.applyFontRecursively(document.body, State.get('currentAdjustment')); log('应用首次字体调整'); }, State.get('firstAdjustmentTime') * 1000); } State.set('isConfigModified', true); if (this.panelCache) { this.updatePanelContent(); } log(`首次调整设置为: ${secs}s`); } } }, { id: 'timer-adjustment', getText: () => `⏱️ ${State.get('intervalSeconds') > 0 ? t.timerAdjustmentEnabled : t.timerAdjustmentDisabled} ${State.get('intervalSeconds') > 0 ? `【${State.get('intervalSeconds')}s】` : ''}`, action: () => { const input = prompt(t.timerPrompt, State.get('intervalSeconds').toString()); const secs = parseInt(input, 10); if (!isNaN(secs)) { State.set('intervalSeconds', secs); if (secs > 0) { State.set('watchDOMChanges', false); if (State.get('observer')) State.get('observer').disconnect(); if (State.get('timer')) clearInterval(State.get('timer')); State.set('timer', setInterval(() => { FontManager.applyFontRecursively(document.body, State.get('currentAdjustment')); }, secs * 1000)); log(`定时调整设置为: ${secs}s`); } else { if (State.get('timer')) clearInterval(State.get('timer')); log('定时调整禁用'); } State.set('isConfigModified', true); if (this.panelCache) { this.updatePanelContent(); } log(`定时调整设置为: ${secs}s`); } } }, { id: 'dynamic-adjustment', getText: () => `🔎 ${State.get('watchDOMChanges') ? t.dynamicAdjustmentEnabled : t.dynamicAdjustmentDisabled}`, action: () => { if (confirm(t.dynamicWatchConfirm)) { State.set('watchDOMChanges', !State.get('watchDOMChanges')); if (State.get('watchDOMChanges')) { State.set('intervalSeconds', 0); if (State.get('timer')) clearInterval(State.get('timer')); const nodeCount = document.body.getElementsByTagName('*').length; const throttleTime = nodeCount > 10000 ? 200 : 100; State.set('observer', new MutationObserver(Utils.throttle(() => { FontManager.applyFontRecursively(document.body, State.get('currentAdjustment')); }, throttleTime))); State.get('observer').observe(document.body, { childList: true, subtree: true }); log('动态调整启用'); } else { if (State.get('observer')) State.get('observer').disconnect(); log('动态调整禁用'); } State.set('isConfigModified', true); if (this.panelCache) { this.updatePanelContent(); } } } }, { id: 'switch-language', getText: () => `🌐 ${t.usageLanguage}: ${State.get('currentLanguage')}`, action: () => { let input; do { input = prompt('zh: 汉语 \t en: English \t ko: 한국어 \t ja: 日本語 \t ru: Русский \t fr: Français \t de: Deutsch \t es: Español \t pt: Português', State.get('currentLanguage')); if (input && !Object.keys(translations).includes(input.trim())) { alert('Invalid language code!'); } } while (input && !Object.keys(translations).includes(input.trim())); if (input && input.trim()) { State.set('currentLanguage', input.trim()); GM_setValue('language', State.get('currentLanguage')); UIManager.updateUI(); if (this.panelCache) { this.panelCache.remove(); this.overlayCache.remove(); this.createFloatingPanel(); } log(`语言切换为: ${State.get('currentLanguage')}`); } } }, { id: 'switch-panel', getText: () => `🎨 ${t.switchPanel}: ${State.get('panelType') === 'tampermonkey' ? t.tampermonkeyPanel : t.floatingPanel}`, action: () => { const newPanelType = State.get('panelType') === 'tampermonkey' ? 'floating' : 'tampermonkey'; GM_setValue(ConfigScopeManager.PANEL_TYPE_KEY, newPanelType); State.set('panelType', newPanelType); if (this.panelCache) { this.panelCache.remove(); this.overlayCache.remove(); this.panelCache = null; this.overlayCache = null; } UIManager.updateUI(); log(`切换到面板类型: ${newPanelType}`); } }, { id: 'show-panel', getText: () => `📅 ${t.showPanel}`, action: () => this.togglePanel(), tampermonkeyOnly: true }, { id: 'currentConfigScope', getText: () => `📍 ${t.currentConfigScope}: ${ConfigScopeManager.getCurrentConfigText(t)}`, action: ConfigManager.deleteCurrentConfig }, { id: 'config-scope', getText: () => `ℹ️ ${t.configScope}: ${ConfigScopeManager.getConfigScopeDisplayText(t)}`, action: ConfigManager.changeConfigScope }, { id: 'save-config', getText: () => `💾 ${State.get('isConfigModified') ? t.saveConfigPending : t.saveConfig}`, action: ConfigManager.saveConfig } ]; }, /** * 创建浮动面板 */ createFloatingPanel() { if (this.panelCache) { log('panelCache 已存在,跳过创建'); return; } const t = translations[State.get('currentLanguage')] || translations.en; const scriptName = GM_info?.script?.name || 'NiceFont'; // 初始化面板 this.panelCache = document.createElement('div'); this.panelCache.id = 'NiceFont_panel'; this.panelCache.setAttribute('data-nicefont-panel', 'true'); this.panelCache.style.position = 'fixed'; this.panelCache.style.width = '300px'; this.panelCache.style.background = '#fff'; this.panelCache.style.border = '1px solid #ccc'; this.panelCache.style.borderRadius = '5px'; this.panelCache.style.padding = '10px'; this.panelCache.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)'; this.panelCache.style.zIndex = '10001'; this.panelCache.style.fontFamily = 'sans-serif'; this.panelCache.style.fontSize = '15px'; this.panelCache.style.userSelect = 'none'; // 初始化遮罩层 this.overlayCache = document.createElement('div'); this.overlayCache.id = 'NiceFont_overlay'; this.overlayCache.style.display = 'none'; // 加载保存的面板位置 const savedPosition = GM_getValue('NiceFont_panelPosition', { top: '50px', right: '20px' }); this.panelCache.style.top = savedPosition.top; this.panelCache.style.right = savedPosition.right; this.panelCache.style.left = 'auto'; // 设置面板内容 this.panelCache.innerHTML = `