// ==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 4.3.0 // @author DD1024z // @description 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 *://*/* // @match file://*/* // @license Apache License 2.0 // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addStyle // @grant GM_info // @grant GM_listValues // @grant GM_download // @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 // ==/UserScript== /** * NiceFont - 网页字体优化工具 * 结构:Utils -> appState -> ConfigScopeManager -> ConfigManager -> FontManager -> UIManager */ (function () { 'use strict'; // iframe 等嵌套页面不执行,避免重复注入 if (window.top !== window.self) { return; } /** 常量:存储键、默认值等 */ const Constants = { DEFAULT_FONT_SIZE: 16, ENABLED_ICON: '✔️', DISABLED_ICON: '✖️', BASE_STORAGE_KEY: 'NiceFont_config', GLOBAL_DEFAULT_KEY: 'NiceFont_global_default_config', EXCLUDED_KEY: 'NiceFont_excluded', PANEL_TYPE_KEY: 'NiceFont_panelType', }; // --- 工具函数模块 --- const Utils = { /** * 节流函数,限制函数调用频率 * @param {Function} fn - 要节流的函数 * @param {number} wait - 节流间隔(毫秒) * @returns {Function} 节流后的函数 */ throttle(fn, wait) { let timeout = null; return function (...args) { if (!timeout) { timeout = setTimeout(() => { fn(...args); timeout = null; }, wait); } }; }, /** * 将字体大小单位转换为像素 * @param {HTMLElement} el - 元素 * @param {string} fontSize - 字体大小(带单位) * @returns {number} 像素值 */ convertToPx(el, fontSize) { if (!fontSize) return Constants.DEFAULT_FONT_SIZE; const units = { 'rem': () => parseFloat(fontSize) * parseFloat(window.getComputedStyle(document.documentElement).fontSize), 'em': () => parseFloat(fontSize) * parseFloat(window.getComputedStyle(el.parentElement).fontSize), '%': () => (parseFloat(fontSize) / 100) * parseFloat(window.getComputedStyle(el.parentElement).fontSize), 'pt': () => parseFloat(fontSize) * 1.3333, 'vw': () => parseFloat(fontSize) * window.innerWidth / 100, 'vh': () => parseFloat(fontSize) * window.innerHeight / 100, 'vmin': () => parseFloat(fontSize) * Math.min(window.innerWidth, window.innerHeight) / 100, 'vmax': () => parseFloat(fontSize) * Math.max(window.innerWidth, window.innerHeight) / 100 }; const unit = fontSize.match(/[a-z%]+$/i)?.[0]; return unit && units[unit] ? units[unit]() : parseFloat(fontSize) || Constants.DEFAULT_FONT_SIZE; }, /** * 检查元素是否包含可见文本 * @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; } }; /** * 应用状态(运行时数据,非持久化) * 持久化由 ConfigManager 通过 GM_getValue/GM_setValue 处理 */ const appState = { fontIncrement: 1, currentFontFamily: 'none', currentAdjustment: 0, dynamicAdjustment: false, intervalSeconds: 0, firstAdjustmentTime: 0, panelType: 'pluginPanel', isConfigModified: false, targetScope: 1, pendingScopeChange: null, observer: null, timer: null, isAutoOpened: false, excludedSelectors: ['i', 'code', 'code *', 'pre', 'pre *', 'svg', 'canvas', 'kbd', 'samp'], textStroke: 0, textShadow: 0, isExcludedSite: false, }; let t; // 多语言文案,init 时按 navigator.language 赋值 let lastSavedSnapshot = null; // 上次保存时的快照,用于判断配置是否被修改 /** 将当前 appState 写入 lastSavedSnapshot */ function updateSavedSnapshot() { lastSavedSnapshot = { resize: appState.currentAdjustment, fontFamily: appState.currentFontFamily, increment: appState.fontIncrement, watcher: appState.dynamicAdjustment, timer: appState.intervalSeconds, firstTime: appState.firstAdjustmentTime, excludedSelectors: JSON.stringify(appState.excludedSelectors), textStroke: appState.textStroke, textShadow: appState.textShadow, }; } /** 对比 lastSavedSnapshot 与 appState,更新 isConfigModified */ function checkConfigModified() { if (appState.pendingScopeChange !== null) { appState.isConfigModified = true; return; } if (!lastSavedSnapshot) { appState.isConfigModified = true; return; } const strokeEq = Math.abs(appState.textStroke - (lastSavedSnapshot.textStroke ?? 0)) < 1e-6; const shadowEq = Math.abs(appState.textShadow - (lastSavedSnapshot.textShadow ?? 0)) < 1e-6; appState.isConfigModified = ( appState.currentAdjustment !== lastSavedSnapshot.resize || appState.currentFontFamily !== lastSavedSnapshot.fontFamily || appState.fontIncrement !== lastSavedSnapshot.increment || appState.dynamicAdjustment !== lastSavedSnapshot.watcher || appState.intervalSeconds !== lastSavedSnapshot.timer || appState.firstAdjustmentTime !== lastSavedSnapshot.firstTime || JSON.stringify(appState.excludedSelectors) !== lastSavedSnapshot.excludedSelectors || !strokeEq || !shadowEq ); } /** 配置范围管理:0=排除本站 1=子域名 2=顶级域 3=全局 */ const ConfigScopeManager = { scopeMap: { 0: 'excludeThisSite', 1: 'subdomain', 2: 'topLevelDomain', 3: 'allWebsites' }, /** 初始化存储键(subdomainKey、topLevelKey、excludedKey) */ initKeys() { this.subdomainKey = `${Constants.BASE_STORAGE_KEY}_${window.location.hostname}`; this.topLevelKey = `${Constants.BASE_STORAGE_KEY}_${Utils.getTopLevelDomain()}`; this.excludedKey = `${Constants.EXCLUDED_KEY}_${window.location.hostname}`; }, /** 根据 targetScope 返回当前配置对应的 GM 存储键 */ getConfigKey() { this.initKeys(); const scope = appState.targetScope; if (scope === 0) return this.excludedKey; if (scope === 1) return this.subdomainKey; if (scope === 2) return this.topLevelKey; return Constants.GLOBAL_DEFAULT_KEY; }, /** 根据存储内容判断实际生效的范围(0/1/2/3) */ getEffectiveScope() { this.initKeys(); const excludedConfig = GM_getValue(this.excludedKey, null); const subdomainConfig = GM_getValue(this.subdomainKey, {}); const topLevelConfig = GM_getValue(this.topLevelKey, {}); const globalConfig = GM_getValue(Constants.GLOBAL_DEFAULT_KEY, {}); if (excludedConfig && excludedConfig.excluded) return 0; 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; }, /** 当前 targetScope 下是否存在有效配置 */ hasConfig() { this.initKeys(); const configKey = this.getConfigKey(); const config = GM_getValue(configKey, null); const hasConfig = !!config && ( (configKey === this.excludedKey && config.excluded) || Object.keys(config).length > 0 ); return hasConfig; }, /** 范围对应的多语言文案 */ getScopeText(scope) { if (scope === 0) return t.excludeThisSite; return scope === 1 ? t.subdomain : scope === 2 ? t.topLevelDomain : t.allWebsites; }, /** 当前配置的显示文本(hostname、*.tld、全部网站等) */ getCurrentConfigText() { this.initKeys(); const excludedConfig = GM_getValue(this.excludedKey, null); const subdomainConfig = GM_getValue(this.subdomainKey, {}); const topLevelConfig = GM_getValue(this.topLevelKey, {}); const globalConfig = GM_getValue(Constants.GLOBAL_DEFAULT_KEY, {}); if (excludedConfig && excludedConfig.excluded) return t.currentConfigScopeExcluded.replace('{hostname}', window.location.hostname); 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; }, /** 配置范围显示文本,含 pendingScopeChange 时显示 "当前 -> 目标" */ getConfigScopeDisplayText() { const effectiveScope = this.getEffectiveScope(); const currentScopeText = this.getScopeText(effectiveScope); const pendingScope = appState.pendingScopeChange; if (pendingScope !== null && pendingScope !== effectiveScope) { const targetScopeText = this.getScopeText(pendingScope); return `${currentScopeText} -> ${targetScopeText}`; } return currentScopeText; }, /** 删除指定范围的配置(scope=0 删 excluded,其他置空对象) */ deleteConfig(scope) { this.initKeys(); let key, target; if (scope === 0) { key = this.excludedKey; target = window.location.hostname; GM_deleteValue(key); } else if (scope === 1) { key = this.subdomainKey; target = window.location.hostname; GM_setValue(key, {}); } else if (scope === 2) { key = this.topLevelKey; target = `*.${Utils.getTopLevelDomain().replace(/^\./, '')}`; GM_setValue(key, {}); } else { key = Constants.GLOBAL_DEFAULT_KEY; target = t.allWebsites; GM_setValue(key, {}); } return true; } }; /** 配置管理:加载/保存/删除/导出导入 */ const ConfigManager = { /** 按优先级加载配置到 appState */ loadConfig() { ConfigScopeManager.initKeys(); let config = GM_getValue(ConfigScopeManager.excludedKey, null); let effectiveScope = 0; let configKey = ConfigScopeManager.excludedKey; if (config && config.excluded) { appState.isExcludedSite = true; appState.currentAdjustment = 0; appState.currentFontFamily = 'none'; appState.textStroke = 0; appState.textShadow = 0; appState.dynamicAdjustment = false; appState.intervalSeconds = 0; appState.firstAdjustmentTime = 0; appState.excludedSelectors = ['i', 'code', 'code *', 'pre', 'pre *', 'svg', 'canvas', 'kbd', 'samp']; appState.targetScope = 0; updateSavedSnapshot(); return; } config = GM_getValue(ConfigScopeManager.subdomainKey, {}); effectiveScope = 1; configKey = ConfigScopeManager.subdomainKey; if (Object.keys(config).length === 0) { config = GM_getValue(ConfigScopeManager.topLevelKey, {}); effectiveScope = 2; configKey = ConfigScopeManager.topLevelKey; if (Object.keys(config).length === 0) { config = GM_getValue(Constants.GLOBAL_DEFAULT_KEY, {}); effectiveScope = Object.keys(config).length > 0 ? 3 : 1; configKey = effectiveScope === 3 ? Constants.GLOBAL_DEFAULT_KEY : ConfigScopeManager.subdomainKey; } } appState.isExcludedSite = false; appState.fontIncrement = (typeof config.increment === 'number' && config.increment >= 0) ? config.increment : 1; appState.currentFontFamily = config.fontFamily || 'none'; appState.currentAdjustment = (typeof config.resize === 'number') ? config.resize : 0; appState.dynamicAdjustment = !!config.watcher; appState.intervalSeconds = (typeof config.timer === 'number' && config.timer >= 0) ? config.timer : 0; appState.firstAdjustmentTime = (typeof config.firstTime === 'number' && config.firstTime >= 0) ? config.firstTime : 0; appState.excludedSelectors = Array.isArray(config.excludedSelectors) && config.excludedSelectors.length > 0 ? config.excludedSelectors : ['i', 'code', 'code *', 'pre', 'pre *', 'svg', 'canvas', 'kbd', 'samp']; appState.textStroke = FontManager.parseStrokeValue(config.textStroke); appState.textShadow = FontManager.parseShadowValue(config.textShadow); appState.targetScope = [0, 1, 2, 3].includes(effectiveScope) ? effectiveScope : 1; updateSavedSnapshot(); }, /** 保存当前 appState 到指定范围,需用户确认 */ saveConfig() { let scope = appState.pendingScopeChange !== null ? appState.pendingScopeChange : appState.targetScope; if (!appState.isConfigModified && appState.pendingScopeChange === null) { scope = appState.targetScope; } if (![0, 1, 2, 3].includes(scope)) { console.warn('[NiceFont] 无效的 scope 值:', scope, '使用默认 scope=1'); scope = 1; } const scopeText = ConfigScopeManager.getScopeText(scope); const target = scope === 0 ? window.location.hostname : 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)) { ConfigScopeManager.initKeys(); const key = scope === 0 ? ConfigScopeManager.excludedKey : scope === 1 ? ConfigScopeManager.subdomainKey : scope === 2 ? ConfigScopeManager.topLevelKey : Constants.GLOBAL_DEFAULT_KEY; if (scope === 0) { GM_setValue(key, { excluded: true }); appState.isExcludedSite = true; appState.currentAdjustment = 0; appState.currentFontFamily = 'none'; appState.textStroke = 0; appState.textShadow = 0; appState.dynamicAdjustment = false; appState.intervalSeconds = 0; appState.firstAdjustmentTime = 0; FontManager.restoreFont(document.body); } else { const config = { increment: appState.fontIncrement, resize: appState.currentAdjustment, watcher: appState.dynamicAdjustment, timer: appState.intervalSeconds, fontFamily: appState.currentFontFamily, firstTime: appState.firstAdjustmentTime, excludedSelectors: appState.excludedSelectors, textStroke: appState.textStroke, textShadow: appState.textShadow }; GM_setValue(key, config); appState.isExcludedSite = false; } appState.isConfigModified = false; appState.targetScope = scope; appState.pendingScopeChange = null; ConfigManager.loadConfig(); setupScheduledAdjustments(); UIManager.updateUI(); } }, /** 修改配置范围(0/1/2/3),可能触发删除确认 */ changeConfigScope() { const effectiveScope = ConfigScopeManager.getEffectiveScope(); const currentScopeText = ConfigScopeManager.getScopeText(effectiveScope); const input = prompt( t.modifyConfigScopePrompt .replace('{scope}', currentScopeText) .replace('{hostname}', window.location.hostname) .replace('{tld}', `*.${Utils.getTopLevelDomain().replace(/^\./, '')}`), appState.targetScope ); const newScope = parseInt(input, 10); if (![0, 1, 2, 3].includes(newScope)) { if (input !== null) alert(t.invalidInput); return; } if (newScope === effectiveScope) { return; } ConfigScopeManager.initKeys(); const hasConfig = effectiveScope === 0 ? !!GM_getValue(ConfigScopeManager.excludedKey, null) : effectiveScope === 1 ? Object.keys(GM_getValue(ConfigScopeManager.subdomainKey, {})).length > 0 : effectiveScope === 2 ? Object.keys(GM_getValue(ConfigScopeManager.topLevelKey, {})).length > 0 : Object.keys(GM_getValue(Constants.GLOBAL_DEFAULT_KEY, {})).length > 0; if (newScope > effectiveScope && hasConfig) { const confirmMessage = effectiveScope === 3 ? `${t.currentConfigScope}: ${ConfigScopeManager.getCurrentConfigText()}\n${t.deleteBeforeScopeChangeConfirm.replace('{scope}', ConfigScopeManager.getScopeText(effectiveScope)).replace(' [{target}]', '')}` : `${t.currentConfigScope}: ${ConfigScopeManager.getCurrentConfigText()}\n${t.deleteBeforeScopeChangeConfirm.replace('{scope}', ConfigScopeManager.getScopeText(effectiveScope)).replace('{target}', ConfigScopeManager.getCurrentConfigText())}`; if (confirm(confirmMessage)) { ConfigScopeManager.deleteConfig(effectiveScope); appState.pendingScopeChange = newScope; appState.targetScope = newScope; checkConfigModified(); UIManager.updateUI(); } } else { appState.pendingScopeChange = newScope; appState.targetScope = newScope; checkConfigModified(); UIManager.updateUI(); } }, /** 删除当前生效范围的配置 */ deleteCurrentConfig() { const effectiveScope = ConfigScopeManager.getEffectiveScope(); const scopeText = ConfigScopeManager.getScopeText(effectiveScope); const target = ConfigScopeManager.getCurrentConfigText(); if (target === t.notConfigured) { return false; } const confirmMessage = effectiveScope === 3 ? `${t.currentConfigScope}: ${target}\n${t.deleteConfigConfirm.replace('{target}', target)}` : `${t.currentConfigScope}: ${target}\n${t.deleteConfigConfirm.replace('{target}', target)}`; if (confirm(confirmMessage)) { ConfigScopeManager.deleteConfig(effectiveScope); appState.targetScope = effectiveScope === 0 ? 1 : 1; appState.pendingScopeChange = null; appState.isExcludedSite = false; appState.isConfigModified = false; ConfigManager.loadConfig(); setupScheduledAdjustments(); UIManager.updateUI(); return true; } return false; }, /** 导出所有 NiceFont_ 开头的 GM 存储为 JSON 文件,优先使用 GM_download */ exportConfig() { const keys = GM_listValues().filter(k => k.startsWith('NiceFont_')); const data = {}; keys.forEach(k => { data[k] = GM_getValue(k, null); }); const json = JSON.stringify({ version: GM_info?.script?.version || '4.2.0', exportedAt: new Date().toISOString(), data }, null, 2); const filename = `NiceFont_config_${new Date().toISOString().slice(0, 10)}.json`; if (typeof GM_download === 'function') { const blob = new Blob([json], { type: 'application/json' }); const blobUrl = URL.createObjectURL(blob); GM_download({ url: blobUrl, name: filename, saveAs: true }); setTimeout(() => URL.revokeObjectURL(blobUrl), 2000); } else { const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 100); } }, /** 从 JSON 文件导入配置,展示新增/修改/删除数量后确认写入 */ importConfig() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json'; input.onchange = () => { const file = input.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const parsed = JSON.parse(reader.result); const data = parsed?.data || parsed; if (typeof data !== 'object') throw new Error('Invalid format'); const importKeys = Object.keys(data).filter(k => k.startsWith('NiceFont_')); if (importKeys.length === 0) throw new Error('No data'); const currentKeys = GM_listValues().filter(k => k.startsWith('NiceFont_')); const added = importKeys.filter(k => !currentKeys.includes(k)); const modified = importKeys.filter(k => { if (!currentKeys.includes(k)) return false; const a = GM_getValue(k, null); const b = data[k]; return JSON.stringify(a) !== JSON.stringify(b); }); const deleted = currentKeys.filter(k => !importKeys.includes(k)); const msg = t.importConfigPreview .replace('{add}', added.length) .replace('{modify}', modified.length) .replace('{delete}', deleted.length); if (!confirm(msg)) return; deleted.forEach(k => GM_deleteValue(k)); importKeys.forEach(k => GM_setValue(k, data[k])); ConfigManager.loadConfig(); updateSavedSnapshot(); setupScheduledAdjustments(); UIManager.updateUI(); alert(t.importConfigSuccess); } catch (e) { console.error('[NiceFont] 导入失败:', e); alert(t.importConfigError); } }; reader.readAsText(file, 'UTF-8'); }; input.click(); }, /** 清空所有 NiceFont_ 配置,需输入 y 确认 */ clearAllConfig() { const input = prompt(t.clearAllConfigConfirm); if (input?.trim().toLowerCase() !== 'y') return; const keys = GM_listValues().filter(k => k.startsWith('NiceFont_')); keys.forEach(k => GM_deleteValue(k)); appState.currentAdjustment = 0; appState.currentFontFamily = 'none'; appState.textStroke = 0; appState.textShadow = 0; appState.targetScope = 1; appState.pendingScopeChange = null; appState.isExcludedSite = false; appState.isConfigModified = false; appState.dynamicAdjustment = false; appState.intervalSeconds = 0; appState.firstAdjustmentTime = 0; ConfigManager.loadConfig(); updateSavedSnapshot(); setupScheduledAdjustments(); UIManager.updateUI(); FontManager.restoreFont(document.body); }, /** 导出/导入入口:prompt 选择 1、2 或 3(插件面板时支持 3 清空) */ exportImportConfig() { const choice = prompt(t.exportImportPrompt); if (choice === '1') ConfigManager.exportConfig(); else if (choice === '2') ConfigManager.importConfig(); else if (choice === '3') ConfigManager.clearAllConfig(); } }; /** 字体管理:遍历 DOM 应用/恢复字体,支持 iframe、Shadow DOM,字体列表可动态扩展 */ const FontManager = { /** 精简字体列表:参考「字体渲染」脚本,保留常用中英文字体,移除对显示无实质影响的 generic 关键字 */ supportFonts: [ 'custom', 'auto', 'none', 'Microsoft YaHei UI', 'Microsoft YaHei', 'PingFang SC', 'Sarasa Gothic SC', 'Source Han Sans SC', 'Hiragino Sans GB', 'HarmonyOS Sans SC', 'LXGW WenKai', 'sans-serif', 'serif', 'monospace', 'Arial', 'Verdana', 'Georgia', 'Times New Roman' ], systemFontsCache: [], styleCache: new WeakMap(), /** 尝试获取系统已安装字体(Chrome 103+,需用户授权);不支持时回退到当前页面已加载字体 */ async loadSystemFonts() { if (typeof window.queryLocalFonts === 'function') { try { const fonts = await window.queryLocalFonts(); const families = [...new Set(fonts.map(f => f.family).filter(Boolean))].sort(); this.systemFontsCache = families; return families; } catch (e) { } } const pageFonts = this.getFontsFromPage(); if (pageFonts.length > 0) { this.systemFontsCache = [...new Set([...this.systemFontsCache, ...pageFonts])].sort(); } return this.systemFontsCache; }, /** 从 document.fonts 获取当前页面已加载的字体(无需权限) */ getFontsFromPage() { if (!document.fonts?.forEach) return []; const seen = new Set(); document.fonts.forEach(ff => { if (ff.family) seen.add(ff.family); }); return [...seen]; }, /** 获取字体列表:supportFonts(含用户添加)+ 系统字体(去重)+ custom 置尾 */ getFontList() { const fromSupport = this.supportFonts.filter(f => f !== 'custom'); const system = this.systemFontsCache.filter(f => !fromSupport.includes(f)); return [...new Set([...fromSupport, ...system]), 'custom']; }, /** 缓存 getComputedStyle,避免重复计算 */ getCachedStyle(el) { if (!this.styleCache.has(el)) { this.styleCache.set(el, window.getComputedStyle(el)); } return this.styleCache.get(el); }, /** 更新排除选择器,校验有效性后写入 appState.excludedSelectors */ updateExcludedSelectors(selectors) { const uniqueSelectors = [...new Set( selectors.split(',') .map(s => s.trim()) .filter(s => s) )]; for (const sel of uniqueSelectors) { try { document.querySelector(sel); } catch (e) { console.error('[NiceFont] 无效的CSS选择器:', sel, e); return false; } } appState.excludedSelectors = uniqueSelectors; return true; }, /** 解析描边值:0-1 数字,兼容旧版 'none' 或字符串 */ parseStrokeValue(v) { if (typeof v === 'number' && !isNaN(v)) return Math.max(0, Math.min(1, v)); if (v === 'none' || v === '' || v == null) return 0; const n = parseFloat(v); return isNaN(n) ? 0 : Math.max(0, Math.min(1, n)); }, /** 解析阴影值:0-4 数字,兼容旧版 'none' 或字符串 */ parseShadowValue(v) { if (typeof v === 'number' && !isNaN(v)) return Math.max(0, Math.min(4, v)); if (v === 'none' || v === '' || v == null) return 0; const n = parseFloat(v); return isNaN(n) ? 0 : Math.max(0, Math.min(4, n)); }, /** 描边数值转 CSS */ getStrokeCSS(v) { return v > 0 ? `${v}px #333` : ''; }, /** 阴影数值转 CSS */ getShadowCSS(v) { return v > 0 ? `1px 1px ${v}px #7C7C7CDD` : ''; }, /** 根据基础字号生成 class 名(四舍五入取整,如 16.3 -> s16) */ getSizeClassBase(basePx) { const n = Math.round(basePx); return Math.max(8, Math.min(96, n)); // 限制 8-96 }, /** 从已有 class 模式类名解析基准字号(px),无则 null */ parseNicefontSizeClassBase(node) { for (const c of node.classList) { const m = /^nicefont-s(\d+)$/.exec(c); if (m) { const n = parseInt(m[1], 10); if (n >= 8 && n <= 96) return n; } } return null; }, /** * 解析用于调整字号的基准 px:优先 data-fontsize-default-fontsize,其次 nicefont-sN,最后当前计算字号 * class 模式不写 data 属性,复用已挂上的 nicefont-sN 作为基准,避免把已叠加 adjustment 的 computed 当成新基准 */ getEffectiveBaseFontSizePx(node, style) { const attr = node.getAttribute('data-fontsize-default-fontsize'); if (attr) { const px = parseFloat(Utils.convertToPx(node, attr)); return isNaN(px) ? null : px; } const fromClass = this.parseNicefontSizeClassBase(node); if (fromClass != null) return fromClass; const current = node.style.fontSize || style.fontSize; if (!current || current === '0px') return null; const px = parseFloat(Utils.convertToPx(node, current)); return isNaN(px) ? null : px; }, /** * 切换渲染方式或重新应用前:去掉 class 档位类;若有 data-nicefont-restore-style 则整段还原为首次记录时的 style 属性,否则按旧逻辑逐条去掉 !important */ stripNodeNiceFontArtifacts(node) { node.classList.remove('nicefont-applied'); for (const c of [...node.classList]) { if (/^nicefont-s\d+$/.test(c)) node.classList.remove(c); } const saved = node.getAttribute('data-nicefont-restore-style'); if (saved !== null) { if (saved === '') { node.removeAttribute('style'); } else { node.setAttribute('style', saved); } } else { for (const p of ['font-family', '-webkit-text-stroke', 'text-stroke', 'text-shadow']) { if (node.style.getPropertyPriority(p) === 'important') { node.style.removeProperty(p); } } if (node.style.getPropertyPriority('font-size') === 'important') { node.style.removeProperty('font-size'); const def = node.getAttribute('data-fontsize-default-fontsize'); if (def) node.style.fontSize = def; } } }, /** 获取或创建全局样式元素,用于 root/class 模式;置于 head 末尾以提高层叠优先级 */ getOrCreateGlobalStyle(doc) { const root = doc.documentElement || doc; if (root.nodeType !== Node.ELEMENT_NODE) return null; let el = doc.getElementById?.('NiceFont-global-styles') || root.querySelector?.('#NiceFont-global-styles'); const parent = doc.head || root; if (!el) { el = doc.createElement('style'); el.id = 'NiceFont-global-styles'; parent.appendChild(el); } else if (el.parentNode) { parent.appendChild(el); } return el; }, /** * contenteditable + 非标准 placeholder:伪元素占位符基准字号(如百度文库 ::after 14px) * 首次用 getComputedStyle(::after) 反推基准(减去当前 adjustment,避免已套用 calc 时存大) */ syncContentEditablePlaceholderBase(node) { if (node.nodeType !== Node.ELEMENT_NODE) return; if (node.getAttribute('contenteditable') !== 'true' || !node.hasAttribute('placeholder')) return; // 仅首次采样并固定基数,避免后续重复计算导致漂移 let basePx = parseFloat(node.getAttribute('data-nicefont-placeholder-base'), 10); if (!basePx || isNaN(basePx) || basePx <= 0) { basePx = 14; try { node.setAttribute('data-nicefont-ph-syncing', '1'); const afterPx = parseFloat(window.getComputedStyle(node, '::after').fontSize); const beforePx = parseFloat(window.getComputedStyle(node, '::before').fontSize); const elPx = parseFloat(window.getComputedStyle(node).fontSize); const raw = afterPx || beforePx || elPx || 14; let b = raw; if (b < 8 || b > 96) b = raw; basePx = Math.round(b * 100) / 100; if (basePx < 1) basePx = 14; } catch (e) { basePx = 14; } finally { node.removeAttribute('data-nicefont-ph-syncing'); } node.setAttribute('data-nicefont-placeholder-base', String(basePx)); } node.style.setProperty('--nicefont-placeholder-base', `${basePx}px`); }, /** 作者是否在 style 属性里写了字体相关声明(用于混合策略:此类节点改走内联强制覆盖) */ hasAuthorInlineFontRelatedStyle(styleAttr) { if (!styleAttr || !String(styleAttr).trim()) return false; return /\b(?:font-size|font-family|font|font-weight|font-variant|font-stretch|line-height)\s*:/i.test(styleAttr); }, /** 全局:字号档位 + .nicefont-applied 字族;描边/阴影为全页一条规则(与 class 分档共用同一 style 标签) */ updateClassModeStyles(doc, sizeBases) { const root = doc.documentElement; if (!root) return; const adj = `${appState.currentAdjustment}px`; const font = appState.currentFontFamily; const strokeCSS = this.getStrokeCSS(appState.textStroke); const shadowCSS = this.getShadowCSS(appState.textShadow); root.style.setProperty('--nicefont-adjustment', adj); root.style.setProperty('--nicefont-family', font !== 'none' ? font : 'inherit'); root.style.setProperty('--nicefont-stroke', strokeCSS || 'none'); root.style.setProperty('--nicefont-shadow', shadowCSS || 'none'); root.classList.add('NiceFont-stroke-shadow'); const sizeRules = (sizeBases || []).map(s => `.nicefont-s${s} { font-size: calc(${s}px + var(--nicefont-adjustment)) !important; }`).join('\n'); const styleEl = this.getOrCreateGlobalStyle(doc); if (styleEl) { const panelNot = ':not(nicefont-panel):not(#NiceFont_panel)'; const appliedRule = `.nicefont-applied { font-family: var(--nicefont-family) !important; }`; const strokeRule = `.NiceFont-stroke-shadow *${panelNot} { -webkit-text-stroke: var(--nicefont-stroke) !important; text-stroke: var(--nicefont-stroke) !important; text-shadow: var(--nicefont-shadow) !important; }`; /* 伪元素字号:calc(每元素变量基准 + 全页 adjustment),变量由 syncContentEditablePlaceholderBase 写入 */ const cePlaceholderRule = `html body .input-normal-wrap [contenteditable="true"][placeholder]:not([data-nicefont-ph-syncing="1"])::before, html body .input-normal-wrap [contenteditable="true"][placeholder]:not([data-nicefont-ph-syncing="1"])::after, html body [contenteditable="true"][placeholder]:not([data-nicefont-ph-syncing="1"])::before, html body [contenteditable="true"][placeholder]:not([data-nicefont-ph-syncing="1"])::after { font-size: calc(var(--nicefont-placeholder-base, 14px) + var(--nicefont-adjustment)) !important; }`; styleEl.textContent = appliedRule + (sizeRules ? '\n' + sizeRules : '') + '\n' + strokeRule + '\n' + cePlaceholderRule; } }, /** 清除全局注入(NiceFont-stroke-shadow、变量、#NiceFont-global-styles) */ clearGlobalStyles(doc) { const root = doc.documentElement; if (root) { root.classList.remove('NiceFont-stroke-shadow'); root.style.removeProperty('--nicefont-adjustment'); root.style.removeProperty('--nicefont-family'); root.style.removeProperty('--nicefont-stroke'); root.style.removeProperty('--nicefont-shadow'); } const el = doc.getElementById?.('NiceFont-global-styles') || doc.querySelector?.('#NiceFont-global-styles'); if (el) el.remove(); }, /** 判断元素是否匹配排除选择器 */ isExcludedElement(el) { return appState.excludedSelectors.some(selector => el.matches(selector)); }, /** 递归遍历 DOM(含 iframe、Shadow DOM),对非排除元素执行 callback */ traverseDOM(el, callback, options) { if (el.nodeType !== Node.ELEMENT_NODE || el.id === 'NiceFont_panel' || el.hasAttribute('data-nicefont-panel') || this.isExcludedElement(el)) { return; } callback(el); if (el.tagName === 'IFRAME') { try { const iframeDoc = el.contentDocument || el.contentWindow.document; if (iframeDoc && iframeDoc.body) { if (options?.clearIframe) { this.clearGlobalStyles(iframeDoc); } else { const bases = options?.classSizeBases || Array.from({ length: 89 }, (_, i) => i + 8); this.updateClassModeStyles(iframeDoc, bases); } this.traverseDOM(iframeDoc.body, callback, options); } } catch (e) { //console.error('[NiceFont] 访问 iframe 失败:', e); } } if (el.shadowRoot) { try { for (const child of el.shadowRoot.children) { if (!this.isExcludedElement(child)) { this.traverseDOM(child, callback, options); } } } catch (e) { // Shadow DOM 访问可能受跨域限制 } } for (const child of el.children) { this.traverseDOM(child, callback, options); } }, /** 递归应用:默认 class 分档 + 全局描边/阴影;若作者已在 style 中声明字体相关则对该节点用内联强制覆盖 */ applyFontRecursively(el, increment) { if (appState.isExcludedSite) return; const font = appState.currentFontFamily; this.clearGlobalStyles(document); const classSizeBases = Array.from({ length: 89 }, (_, i) => i + 8); this.updateClassModeStyles(document, classSizeBases); const opts = { classSizeBases }; this.traverseDOM(el, (node) => { this.styleCache.delete(node); const originalStyleAttrBeforeStrip = node.getAttribute('style') ?? ''; const useInlineFont = this.hasAuthorInlineFontRelatedStyle(originalStyleAttrBeforeStrip); const styleBeforeStrip = window.getComputedStyle(node); const hadDataDefault = node.hasAttribute('data-fontsize-default-fontsize'); const fromClassBeforeStrip = this.parseNicefontSizeClassBase(node); const baseFontSize = this.getEffectiveBaseFontSizePx(node, styleBeforeStrip); this.stripNodeNiceFontArtifacts(node); this.styleCache.delete(node); const style = this.getCachedStyle(node); const isVisible = style.display !== 'none' && style.visibility !== 'hidden'; const isFormControl = node.tagName === 'TEXTAREA' || node.tagName === 'INPUT'; const isCePlaceholder = node.getAttribute('contenteditable') === 'true' && node.hasAttribute('placeholder'); if ((Utils.hasVisibleText(node) || isFormControl || isCePlaceholder) && isVisible) { if (isCePlaceholder) { this.syncContentEditablePlaceholderBase(node); } const currentFontSize = node.style.fontSize || style.fontSize; if (useInlineFont && !hadDataDefault) { node.setAttribute( 'data-fontsize-default-fontsize', fromClassBeforeStrip != null ? `${fromClassBeforeStrip}px` : currentFontSize ); node.setAttribute('data-nicefont-restore-style', originalStyleAttrBeforeStrip); } if (currentFontSize != '0px') { if (baseFontSize != null && !isNaN(baseFontSize)) { if (useInlineFont) { node.style.setProperty('font-size', `${baseFontSize + increment}px`, 'important'); if (font !== 'none') { node.style.setProperty('font-family', font, 'important'); } else { node.style.removeProperty('font-family'); } } else { const s = this.getSizeClassBase(baseFontSize); node.classList.add('nicefont-applied', `nicefont-s${s}`); } } } } }, opts); }, /** 恢复字体:若有 data-nicefont-restore-style 则还原为首次记录时的 style;否则按旧逻辑逐项移除 */ restoreFont(el) { appState.currentAdjustment = 0; appState.currentFontFamily = 'none'; appState.textStroke = 0; appState.textShadow = 0; appState.intervalSeconds = 0; appState.firstAdjustmentTime = 0; this.clearGlobalStyles(document); this.traverseDOM(el, (node) => { const isFormControl = node.tagName === 'TEXTAREA' || node.tagName === 'INPUT'; const isCePh = node.getAttribute('contenteditable') === 'true' && node.hasAttribute('placeholder'); if (Utils.hasVisibleText(node) || isFormControl || isCePh) { if (node.hasAttribute('data-nicefont-placeholder-base')) { node.removeAttribute('data-nicefont-placeholder-base'); node.style.removeProperty('--nicefont-placeholder-base'); } node.classList.remove('nicefont-applied'); for (let i = 8; i <= 96; i++) node.classList.remove(`nicefont-s${i}`); const saved = node.getAttribute('data-nicefont-restore-style'); if (saved !== null) { if (saved === '') { node.removeAttribute('style'); } else { node.setAttribute('style', saved); } node.removeAttribute('data-fontsize-default-fontsize'); node.removeAttribute('data-nicefont-restore-style'); } else { const defaultSize = node.getAttribute('data-fontsize-default-fontsize'); if (defaultSize) { node.style.fontSize = defaultSize; } else { node.style.removeProperty('font-size'); } node.removeAttribute('data-fontsize-default-fontsize'); node.style.removeProperty('font-family'); node.style.removeProperty('-webkit-text-stroke'); node.style.removeProperty('text-stroke'); node.style.removeProperty('text-shadow'); if (!node.style.cssText.trim()) { node.removeAttribute('style'); } } this.styleCache.delete(node); } }, { clearIframe: true }); }, /** 调整字体大小(累加 increment),并应用至页面 */ changeFontSize(increment) { if (appState.isExcludedSite) return; appState.currentAdjustment = appState.currentAdjustment + increment; this.applyFontRecursively(document.body, appState.currentAdjustment); checkConfigModified(); UIManager.updateUI(); } }; /** * 界面管理:插件菜单、浮动面板、命令配置 * 支持 pluginPanel(油猴菜单)与 floatingPanel(页面内浮动) */ const UIManager = { menuHandles: [], panelCache: null, /** 返回所有命令配置(id、getText、action、displayInPluginPanel 等) */ getCommandsConfig() { const commands = [ { id: 'setFontFamily', getText: () => `🔠 ${t.setFontFamily}: ${appState.currentFontFamily}`, action: () => { if (appState.panelType === 'pluginPanel') { const input = prompt(`${t.setFontFamilyPrompt}\n\n${t.supportFontFamily}\n${FontManager.getFontList().slice(0, -1).join(', ')}`, appState.currentFontFamily === 'none' ? '' : appState.currentFontFamily); if (input && input.trim()) { const newFont = input.trim(); appState.currentFontFamily = newFont; if (!FontManager.supportFonts.includes(newFont)) { FontManager.supportFonts.splice(FontManager.supportFonts.length - 1, 0, newFont); } FontManager.applyFontRecursively(document.body, appState.currentAdjustment); checkConfigModified(); UIManager.updateUI(); } } else { const shadow = UIManager.panelCache?.shadowRoot; shadow?.querySelector('#NiceFont_stroke-slider-wrap')?.remove(); shadow?.querySelector('#NiceFont_shadow-slider-wrap')?.remove(); let select = shadow?.querySelector('#NiceFont_font-family'); if (select) { select.remove(); document.removeEventListener('click', UIManager.closeDropdown); UIManager.closeDropdown = null; return; } select = document.createElement('select'); select.id = 'NiceFont_font-family'; select.className = 'font-family-select'; const fontToCss = f => f.includes("'") ? `"${f.replace(/"/g, '\\"')}"` : (f.includes(' ') ? `'${f}'` : f); const renderFontOptions = () => { const list = FontManager.getFontList(); const skipPreview = ['none', 'auto', 'custom']; select.innerHTML = list.map(font => { const label = font === 'custom' ? t.customInput : font; const ff = skipPreview.includes(font) ? '' : ` style="font-family: ${fontToCss(font)}, sans-serif"`; return ``; }).join(''); const cur = appState.currentFontFamily; select.style.fontFamily = !skipPreview.includes(cur) ? `${fontToCss(cur)}, sans-serif` : ''; }; renderFontOptions(); FontManager.loadSystemFonts().then(loaded => { if (loaded.length > 0 && select.parentNode) renderFontOptions(); }); const btn = UIManager.panelCache?.shadowRoot?.querySelector('#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.getFontList().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; option.style.fontFamily = `${fontToCss(newFont)}, sans-serif`; select.insertBefore(option, select.lastChild); } appState.currentFontFamily = newFont; select.value = newFont; } else { select.value = appState.currentFontFamily; select.remove(); document.removeEventListener('click', UIManager.closeDropdown); UIManager.closeDropdown = null; return; } } else { appState.currentFontFamily = selectedFont; } FontManager.applyFontRecursively(document.body, appState.currentAdjustment); checkConfigModified(); UIManager.updateUI(); select.remove(); document.removeEventListener('click', UIManager.closeDropdown); UIManager.closeDropdown = null; }); UIManager.closeDropdown = (event) => { if (!select.contains(event.target) && !btn.contains(event.target)) { select.remove(); document.removeEventListener('click', UIManager.closeDropdown); UIManager.closeDropdown = null; } }; document.addEventListener('click', UIManager.closeDropdown); } } }, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'setTextStroke', getText: () => `✏️ ${t.setTextStroke}: ${appState.textStroke > 0 ? appState.textStroke.toFixed(2) : t.none}`, action: () => { if (appState.panelType === 'pluginPanel') { const input = prompt(t.setTextStrokePrompt, appState.textStroke.toString()); if (input !== null) { const val = FontManager.parseStrokeValue(input.trim()); appState.textStroke = val; FontManager.applyFontRecursively(document.body, appState.currentAdjustment); checkConfigModified(); UIManager.updateUI(); } } else { UIManager.showStrokeSlider(); } }, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'setTextShadow', getText: () => `🌑 ${t.setTextShadow}: ${appState.textShadow > 0 ? appState.textShadow.toFixed(2) : t.none}`, action: () => { if (appState.panelType === 'pluginPanel') { const input = prompt(t.setTextShadowPrompt, appState.textShadow.toString()); if (input !== null) { const val = FontManager.parseShadowValue(input.trim()); appState.textShadow = val; FontManager.applyFontRecursively(document.body, appState.currentAdjustment); checkConfigModified(); UIManager.updateUI(); } } else { UIManager.showShadowSlider(); } }, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'status', getText: () => `📏 ${t.fontSizeAdjustment}: ${appState.currentAdjustment >= 0 ? '+' : ''}${appState.currentAdjustment}px`, action: () => { }, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'increase', getText: () => `🔼 ${t.increase}`, action: () => FontManager.changeFontSize(appState.fontIncrement), autoClose: false, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'decrease', getText: () => `🔽 ${t.decrease}`, action: () => FontManager.changeFontSize(-appState.fontIncrement), autoClose: false, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'restore', getText: () => `♻️ ${t.restore}`, action: () => { FontManager.restoreFont(document.body); checkConfigModified(); UIManager.updateUI(); }, autoClose: false, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'first-adjustment', getText: () => { const statusIcon = appState.firstAdjustmentTime > 0 ? Constants.ENABLED_ICON : Constants.DISABLED_ICON; const timeText = appState.firstAdjustmentTime > 0 ? `【${appState.firstAdjustmentTime}s】` : ''; return `1️⃣ ${t.firstAdjustment}: ${statusIcon}${timeText}`; }, action: () => { const input = prompt(t.firstAdjustmentConfirm, appState.firstAdjustmentTime.toString()); const secs = parseInt(input, 10); if (!isNaN(secs) && secs >= 0) { appState.firstAdjustmentTime = secs; if (secs > 0) { appState.intervalSeconds = 0; appState.dynamicAdjustment = false; checkConfigModified(); } if (this.panelCache) { this.updatePanelContent(); } } else { appState.firstAdjustmentTime = 0; } }, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'timer-adjustment', getText: () => { const statusIcon = appState.intervalSeconds > 0 ? Constants.ENABLED_ICON : Constants.DISABLED_ICON; const timeText = appState.intervalSeconds > 0 ? `【${appState.intervalSeconds}s】` : ''; return `⏱️ ${t.timerAdjustment}: ${statusIcon}${timeText}`; }, action: () => { const input = prompt(t.timerPrompt, appState.intervalSeconds.toString()); const secs = parseInt(input, 10); if (!isNaN(secs) && secs >= 0) { appState.intervalSeconds = secs; if (secs > 0) { appState.firstAdjustmentTime = 0; appState.dynamicAdjustment = false; checkConfigModified(); } if (this.panelCache) { this.updatePanelContent(); } } else { appState.intervalSeconds = 0; } }, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'dynamic-adjustment', getText: () => { const statusIcon = appState.dynamicAdjustment ? Constants.ENABLED_ICON : Constants.DISABLED_ICON; return `🔎 ${t.dynamicAdjustment}: ${statusIcon}`; }, action: () => { if (confirm(t.dynamicWatchConfirm)) { appState.dynamicAdjustment = !appState.dynamicAdjustment; if (appState.dynamicAdjustment) { appState.firstAdjustmentTime = 0; appState.intervalSeconds = 0; checkConfigModified(); } if (this.panelCache) { this.updatePanelContent(); } } }, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'exclude-elements', getText: () => { const maxDisplayLength = 23; const selectors = Array.isArray(appState.excludedSelectors) ? appState.excludedSelectors : ['i', 'code', 'code *', 'pre', 'pre *', 'svg', 'canvas', 'kbd', 'samp']; const selectorsText = selectors.join(', '); const displayText = selectorsText.length > maxDisplayLength ? selectorsText.substring(0, maxDisplayLength - 3) + '...' : selectorsText; return `🚫 ${t.excludeElements}: ${displayText || t.none}`; }, action: () => { const input = prompt(t.excludeElementsPrompt, appState.excludedSelectors.join(', ')); if (input !== null && input.trim()) { if (FontManager.updateExcludedSelectors(input)) { FontManager.applyFontRecursively(document.body, appState.currentAdjustment); checkConfigModified(); UIManager.updateUI(); } else { alert(t.invalidSelectorAlert); } } }, title: appState.excludedSelectors, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'switch-panel', getText: () => `🎨 ${t.switchPanel}: ${appState.panelType === 'pluginPanel' ? t.pluginPanel : t.floatingPanel}`, action: () => { const newPanelType = appState.panelType === 'pluginPanel' ? 'floatingPanel' : 'pluginPanel'; if (newPanelType === 'floatingPanel') { const shouldAutoOpen = confirm(t.autoOpenFloatingPanelPrompt); GM_setValue('NiceFont_autoOpenPageMenu', !shouldAutoOpen); } GM_setValue(Constants.PANEL_TYPE_KEY, newPanelType); appState.panelType = newPanelType; if (this.panelCache) { this.panelCache.remove(); this.panelCache = null; } if (newPanelType === 'floatingPanel') { this.createFloatingPanel(); if (this.panelCache && this.panelCache.shadowRoot) { const shadow = this.panelCache.shadowRoot; const panelContainer = shadow.querySelector('div'); if (panelContainer) { // 显式切换到浮动面板时始终打开一次;确认框只写入 NiceFont_autoOpenPageMenu,影响后续无配置页的自动弹出 panelContainer.style.display = 'block'; appState.isAutoOpened = true; } } else { console.error('[NiceFont] panelCache 或 shadowRoot 未正确初始化,面板显示失败'); } } UIManager.updateUI(); }, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'show-panel', getText: () => `🔣 ${t.showPanel}`, action: () => this.togglePanel(), displayInPluginPanel: true, displayInFloatingPanel: false }, { id: 'currentConfigScope', getText: () => `📍 ${t.currentConfigScope}: ${ConfigScopeManager.getCurrentConfigText()}`, action: ConfigManager.deleteCurrentConfig, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'config-scope', getText: () => `ℹ️ ${t.modifyConfigScope}: ${ConfigScopeManager.getConfigScopeDisplayText()}`, action: ConfigManager.changeConfigScope, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'export-import-config', getText: () => `📋 ${t.exportImportConfig}`, getFloatingPanelHtml: () => { const m = t.exportImportConfig.match(/^([^/]+)\/(.+)$/); const exportL = m ? m[1].trim() : t.exportLink; const rest = m ? m[2] : t.importLink + t.exportImportConfigSuffix; const suffix = t.exportImportConfigSuffix; const importL = suffix && rest.endsWith(suffix) ? rest.slice(0, -suffix.length).trim() : rest; const suffixDisplay = suffix && rest.endsWith(suffix) ? suffix : ''; const clearLabel = suffixDisplay || t.clearAllConfigLabel; const suffixHtml = clearLabel ? `${suffixDisplay ? '' : ' '}${clearLabel}` : ''; return `📋 ${exportL}/${importL}${suffixHtml}`; }, action: ConfigManager.exportImportConfig, displayInPluginPanel: true, displayInFloatingPanel: true }, { id: 'save-config', getText: () => `💾 ${appState.isConfigModified ? t.saveConfigPending : t.saveConfig}`, action: ConfigManager.saveConfig, displayInPluginPanel: true, displayInFloatingPanel: true } ]; // 如果站点被排除,只显示 currentConfigScope if (appState.isExcludedSite && appState.panelType === 'pluginPanel') { return commands.filter(cmd => cmd.id === 'currentConfigScope'); } return commands; }, /** 刷新油猴菜单项文案 */ updatePluginPanel() { this.menuHandles.forEach(handle => { try { GM_unregisterMenuCommand(handle); } catch (e) { console.error('[NiceFont] 取消注册菜单失败:', e); } }); this.menuHandles = []; const commands = appState.panelType === 'pluginPanel' ? this.getCommandsConfig().filter(cmd => cmd.id !== 'show-panel') : this.getCommandsConfig().filter(cmd => ['switch-panel', 'show-panel'].includes(cmd.id)); commands.forEach(cmd => { const handle = GM_registerMenuCommand(cmd.getText(), () => { cmd.action(); this.updatePluginPanel(); }, { autoClose: cmd.autoClose, title: cmd.title }); this.menuHandles.push(handle); }); }, /** 刷新浮动面板内的按钮列表 */ updatePanelContent() { if (!this.panelCache || !this.panelCache.shadowRoot) { return; } const scriptName = t.panelTitle; const shadow = this.panelCache.shadowRoot; const titleDiv = shadow.querySelector('.NiceFont_title'); if (titleDiv) { titleDiv.textContent = scriptName; } else { console.error('[NiceFont] 未找到 .NiceFont_title,无法更新标题'); } const contentContainer = shadow.querySelector('.NiceFont_content'); if (contentContainer) { contentContainer.innerHTML = this.getCommandsConfig() .filter(cmd => cmd.displayInFloatingPanel && (!appState.isExcludedSite || ['currentConfigScope', 'config-scope', 'export-import-config', 'save-config', 'switch-panel'].includes(cmd.id))) .map(cmd => { const html = cmd.getFloatingPanelHtml ? cmd.getFloatingPanelHtml() : cmd.getText(); return `