// ==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.2 // @author DD1024z // @description NiceFont: 是一款优化网页字体显示的工具,让浏览更清晰、舒适!“真正调整字体,而非页面缩放,拒绝将就”!可直接修改网页的字体大小与风格,保存你的字体设置,轻松应用到每个网页,支持首次、定时或动态调整字体,适配子域名、整站或全局设置,几乎兼容所有网站! // @description:zh-CN NiceFont: 是一款优化网页字体显示的工具,让浏览更清晰、舒适!“真正调整字体,而非页面缩放,拒绝将就”!可直接修改网页的字体大小与风格,保存你的字体设置,轻松应用到每个网页,支持首次、定时或动态调整字体,适配子域名、整站或全局设置,几乎兼容所有网站! // @description:zh-TW NiceFont:優化網頁字體顯示的工具,瀏覽更清晰、舒適!「真正調整字體,非頁面縮放,拒絕將就」!直接修改字體大小與風格,儲存設定,輕鬆應用於各網頁,支援首次、定時或動態調整,適配子域名或全局設定,幾乎相容所有網站! // @description:en NiceFont: Optimize web font display for clear, comfortable browsing! "Adjusts fonts directly, not page scaling—no compromises!" Modify font size & style, save settings, apply to all pages. Supports one-time, scheduled, or dynamic adjustments, for subdomains or globally. Works on nearly all sites! // @description:ko NiceFont: 웹 폰트 표시를 최적화하여 선명하고 편안한 브라우징! "페이지를 스케일링하지 않고 폰트를 조정—타협 없음!" 폰트 크기와 스타일을 수정, 설정 저장, 모든 페이지에 적용. 최초, 정기, 동적 조정 지원, 서브도메인 또는 전역 설정. 거의 모든 사이트 호환! // @description:ja NiceFont:ウェブフォントを最適化し、クリアで快適な閲覧を!「ページスケーリング不要、フォントを直接調整—妥協なし!」フォントサイズとスタイルを変更、設定を保存、全ページに適用。初回、定期、動的調整をサポート、サブドメインやグローバル設定に対応。ほぼ全サイト対応! // @description:ru NiceFont: Оптимизирует веб-шрифты для четкого и удобного чтения! "Регулирует шрифты, а не масштабирует страницу — никаких компромиссов!" Изменяет размер и стиль шрифта, сохраняет настройки, применяет ко всем страницам. Поддерживает разовые, плановые или динамические настройки, для поддоменов или глобально. Работает почти на всех сайтах! // @description:fr NiceFont : Optimisez l'affichage des polices web pour une navigation claire et confortable ! « Ajuste les polices directement, pas de zoom de page — sans compromis ! » Modifie taille et style, enregistre les paramètres, applique à toutes les pages. Supporte ajustements uniques, programmés ou dynamiques, pour sous-domaines ou global. Compatible avec presque tous les sites ! // @description:de NiceFont: Optimiert Webschrift für klares, angenehmes Surfen! "Passt Schriften direkt an, ohne Seiten-Skalierung — keine Kompromisse!" Ändert Schriftgröße und -stil, speichert Einstellungen, wendet sie auf alle Seiten an. Unterstützt einmalige, geplante oder dynamische Anpassungen, für Subdomains oder global. Kompatibel mit fast allen Websites! // @description:es NiceFont: Optimiza fuentes web para una navegación clara y cómoda. "Ajusta fuentes directamente, sin escalar página — ¡sin concesiones!" Modifica tamaño y estilo, guarda configuraciones, aplica a todas las páginas. Admite ajustes únicos, programados o dinámicos, para subdominios o global. Compatible con casi todos los sitios. // @description:pt NiceFont: Otimiza fontes web para navegação clara e confortável! "Ajusta fontes diretamente, sem escalonar página — sem concessões!" Modifica tamanho e estilo, salva configurações, aplica a todas as páginas. Suporta ajustes únicos, agendados ou dinâmicos, para subdomínios ou global. Compatível com quase todos os sites! // @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 = false; // 关闭跟踪常量 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', 'none', 'Arial', 'cursive', 'fangsong', 'fantasy', 'monospace', '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 && iframeDoc.body) { const font = State.get('currentFontFamily'); if (font !== 'none') { iframeDoc.documentElement.style.fontFamily = font; } else { iframeDoc.documentElement.style.removeProperty('font-family'); } this.traverseDOM(iframeDoc.body, callback); } } 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) { const font = State.get('currentFontFamily'); 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`; } if (font !== 'none') { node.style.fontFamily = font; // 修复:fontFfamily -> fontFamily } else { node.style.removeProperty('font-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'); }); // 重置关闭跟踪状态 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; if (State.get('panelType') === 'tampermonkey') { // 油猴菜单模式下直接弹出提示框 const input = prompt(`${t.setFontFamilyPrompt}\n\n${t.supportFontFamily}\n${FontManager.supportFonts.join(', ')}`, State.get('currentFontFamily') === 'none' ? '' : State.get('currentFontFamily')); if (input && input.trim()) { const newFont = input.trim(); if (!FontManager.supportFonts.includes(newFont)) { FontManager.supportFonts.splice(FontManager.supportFonts.length - 1, 0, newFont); } State.set('currentFontFamily', newFont); FontManager.applyFontRecursively(document.body, State.get('currentAdjustment')); State.set('isConfigModified', true); UIManager.updateUI(); log(`字体类型设置为: ${newFont}`); } else { log('取消字体输入'); } } else { // 浮动面板模式保持原有逻辑 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()) { const newLanguage = input.trim(); State.set('currentLanguage', newLanguage); GM_setValue('language', newLanguage); log(`语言切换为: ${newLanguage}`); // 更新现有面板内容,而不是销毁 if (UIManager.panelCache && document.body.contains(UIManager.panelCache)) { UIManager.updatePanelContent(); // 根据配置决定是否立即显示面板 const autoShow = GM_getValue('NiceFont_autoShowAfterLanguageSwitch', true); if (autoShow) { UIManager.panelCache.style.display = 'block'; UIManager.overlayCache.style.display = 'block'; log('语言切换后自动显示面板'); } } else { // 如果面板不存在,创建并根据配置显示 UIManager.createFloatingPanel(); if (UIManager.panelCache) { const autoShow = GM_getValue('NiceFont_autoShowAfterLanguageSwitch', true); UIManager.panelCache.style.display = autoShow ? 'block' : 'none'; UIManager.overlayCache.style.display = autoShow ? 'block' : 'none'; log(`面板创建后,显示状态: ${autoShow ? 'block' : 'none'}`); } else { console.error('[NiceFont] 语言切换后创建面板失败'); } } UIManager.updateUI(); } } }, { 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; log('移除现有浮动面板'); } if (newPanelType === 'floating') { // 直接创建并显示浮动面板,不检查配置源 this.createFloatingPanel(); if (this.panelCache) { this.panelCache.style.display = 'block'; this.overlayCache.style.display = 'block'; log('直接创建并显示浮动面板(切换到网页菜单模式)'); } } 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 && document.body.contains(this.panelCache)) { log('panelCache 已存在且在 DOM 中,跳过创建'); return; } // 清理现有面板 if (this.panelCache) { this.panelCache.remove(); this.overlayCache.remove(); this.panelCache = null; this.overlayCache = null; log('清理现有 panelCache'); } const t = translations[State.get('currentLanguage')] || translations.en; const scriptName = GM_info?.script?.name || 'NiceFont'; // 确保 DOM 已就绪 if (!document.body) { console.error('[NiceFont] document.body 不可用,延迟创建面板'); return; } // 初始化面板 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.panelCache.style.display = '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 = `