// ==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
// ==/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 =>
`<option value="${font}" ${font === State.get('currentFontFamily') ? 'selected' : ''}>${font === 'custom' ? (State.get('currentLanguage') === 'zh' ? '手动输入' : 'Custom Input') : font}</option>`
).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 = `
<div class="NiceFont_header" style="position: relative; z-index: 10002; display: flex; align-items: center; justify-content: space-between;">
<div style="font-size: 16px; text-align: left; flex-grow: 1; cursor: grab; margin: 5px; font-weight: bold;">${scriptName}</div>
<button class="NiceFont_close-btn" id="NiceFont_close-btn" style="border: none; border-radius: 3px; padding: 1px 6px; cursor: pointer; line-height: 16px; font-size: 12px; background: none; color: #000;">✖️</button>
</div>
<div class="NiceFont_content"></div>
`;
// 填充内容区域
this.updatePanelContent();
// 添加到 DOM
try {
document.body.appendChild(this.overlayCache);
document.body.appendChild(this.panelCache);
log('浮动面板创建并添加到 DOM');
} catch (e) {
console.error('[NiceFont] 添加面板到 DOM 失败:', e);
this.panelCache = null;
this.overlayCache = null;
return;
}
// 获取 header 元素
const header = this.panelCache.querySelector('.NiceFont_header');
if (!header) {
console.error('[NiceFont] 未找到 .NiceFont_header,无法绑定拖拽事件');
this.panelCache.remove();
this.overlayCache.remove();
this.panelCache = null;
this.overlayCache = null;
return;
}
// 添加拖动功能
let isDragging = false;
let initialX;
let initialY;
let rafId = null;
header.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('NiceFont_close-btn')) {
log('点击关闭按钮,忽略拖拽');
return;
}
isDragging = true;
initialX = e.clientX + parseFloat(this.panelCache.style.right || '0');
initialY = e.clientY - parseFloat(this.panelCache.style.top || '0');
header.style.cursor = 'grabbing';
log('开始拖拽');
e.preventDefault();
e.stopPropagation();
}, { capture: true, passive: false });
document.addEventListener('mousemove', (e) => {
if (isDragging) {
e.preventDefault();
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
let newX = initialX - e.clientX;
let newY = e.clientY - initialY;
newX = Math.max(0, Math.min(newX, window.innerWidth - this.panelCache.offsetWidth));
newY = Math.max(0, Math.min(newY, window.innerHeight - this.panelCache.offsetHeight));
this.panelCache.style.right = `${newX}px`;
this.panelCache.style.top = `${newY}px`;
this.panelCache.style.left = 'auto';
log(`拖拽中: right=${newX}px, top=${newY}px`);
});
}
}, { capture: true, passive: false });
document.addEventListener('mouseup', (e) => {
if (isDragging) {
isDragging = false;
header.style.cursor = 'grab';
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
GM_setValue('NiceFont_panelPosition', {
top: this.panelCache.style.top,
right: this.panelCache.style.right
});
log('拖拽结束, 面板位置保存:', this.panelCache.style.top, this.panelCache.style.right);
e.stopPropagation();
}
}, { capture: true, passive: false });
// 添加长按功能
let longPressTimer = null;
const startLongPress = (action, interval = 100) => {
action();
longPressTimer = setInterval(action, interval);
};
const stopLongPress = () => {
if (longPressTimer) {
clearInterval(longPressTimer);
longPressTimer = null;
}
};
this.panelCache.addEventListener('mousedown', (e) => {
const btn = e.target.closest('.action-btn');
if (btn) {
const commandId = btn.id.replace('NiceFont_', '');
if (commandId === 'increase' || commandId === 'decrease') {
const command = this.getCommandsConfig().find(c => c.id === commandId);
if (command) {
startLongPress(command.action);
}
}
}
}, { capture: false });
this.panelCache.addEventListener('mouseup', stopLongPress, { capture: false });
this.panelCache.addEventListener('mouseleave', stopLongPress, { capture: false });
// 绑定点击事件
this.panelCache.addEventListener('click', (e) => {
const btn = e.target.closest('.action-btn');
if (btn) {
const command = this.getCommandsConfig().find(c => c.id === btn.id.replace('NiceFont_', ''));
if (command && command.id !== 'increase' && command.id !== 'decrease') {
log(`执行命令: ${command.id}`);
command.action();
}
}
if (e.target.id === 'NiceFont_close-btn') {
this.panelCache.style.display = 'none';
this.overlayCache.style.display = 'none';
// 检查是否因无配置自动弹出
if (!ConfigScopeManager.hasConfig()) {
const now = Date.now();
const lastCloseTime = GM_getValue('NiceFont_lastCloseTime', 0);
let closeCount = GM_getValue('NiceFont_closeCount', 0);
if (now - lastCloseTime > CLOSE_TRACKING_WINDOW) {
closeCount = 0;
log('关闭计数重置(超出时间窗口)');
}
closeCount += 1;
GM_setValue('NiceFont_closeCount', closeCount);
GM_setValue('NiceFont_lastCloseTime', now);
log(`面板关闭(无配置源): closeCount=${closeCount}, lastCloseTime=${now}`);
if (closeCount >= CLOSE_COUNT_THRESHOLD) {
GM_setValue('NiceFont_autoOpenDisabled', true);
log('禁用无配置源自动弹出(连续关闭达到阈值)');
}
} else {
log('面板关闭(有配置源)');
}
}
e.stopPropagation();
}, { capture: false });
},
/**
* 更新面板内容
*/
updatePanelContent() {
if (!this.panelCache) {
log('panelCache 不存在,跳过更新内容');
return;
}
const t = translations[State.get('currentLanguage')] || translations.en;
const scriptName = GM_info?.script?.name || 'NiceFont';
// 更新标题
const headerDiv = this.panelCache.querySelector('.NiceFont_header > div');
if (headerDiv) {
headerDiv.textContent = scriptName;
log('面板标题更新为:', scriptName);
} else {
console.error('[NiceFont] 未找到 .NiceFont_header > div,无法更新标题');
}
// 更新内容区域
const contentContainer = this.panelCache.querySelector('.NiceFont_content');
if (contentContainer) {
contentContainer.innerHTML = this.getCommandsConfig()
.filter(cmd => !cmd.tampermonkeyOnly)
.map(cmd =>
`<div class="action-btn" id="NiceFont_${cmd.id}">${cmd.getText()}</div>`
).join('');
log('面板内容更新成功');
} else {
console.error('[NiceFont] 未找到 .NiceFont_content,无法更新内容');
}
},
/**
* 更新油猴菜单
*/
updateTampermonkeyMenu() {
this.menuHandles.forEach(handle => {
try {
GM_unregisterMenuCommand(handle);
} catch (e) {
console.error('[NiceFont] 取消注册菜单失败:', e);
}
});
this.menuHandles = [];
const commands = State.get('panelType') === 'tampermonkey'
? 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();
log(`执行油猴菜单命令: ${cmd.id}`);
}, { autoClose: cmd.autoClose });
this.menuHandles.push(handle);
log(`注册菜单: ${cmd.id}`);
});
},
/**
* 显示/隐藏面板
*/
togglePanel() {
if (State.get('panelType') !== 'floating') {
log('非浮动面板模式,忽略 togglePanel');
return;
}
const now = Date.now();
if (now - this.lastToggleTime < 300) {
log('togglePanel 防抖,忽略快速重复调用');
return;
}
this.lastToggleTime = now;
// 确保 DOM 已就绪
if (!document.body) {
console.error('[NiceFont] document.body 不可用,延迟 togglePanel');
return;
}
// 如果 panelCache 不存在或未附加到 DOM,强制创建
if (!this.panelCache || !document.body.contains(this.panelCache)) {
log('panelCache 不存在或未附加到 DOM,尝试重新创建');
this.createFloatingPanel();
if (!this.panelCache) {
console.error('[NiceFont] 面板创建失败,检查 createFloatingPanel');
return;
}
}
// 切换显示状态
const isHidden = this.panelCache.style.display === 'none';
const display = isHidden ? 'block' : 'none';
this.panelCache.style.display = display;
this.overlayCache.style.display = display;
log(`面板显示状态: ${display}`);
// 如果显示面板,更新内容
if (display === 'block') {
this.updatePanelContent();
}
},
/**
* 更新界面
*/
updateUI() {
log('调用 updateUI, panelType:', State.get('panelType'));
if (State.get('panelType') === 'tampermonkey') {
this.updateTampermonkeyMenu();
if (this.panelCache) {
this.panelCache.remove();
this.overlayCache.remove();
this.panelCache = null;
this.overlayCache = null;
log('移除浮动面板(切换到油猴菜单)');
}
} else {
this.updateTampermonkeyMenu();
// 如果 panelCache 存在,更新内容;否则等待 togglePanel 创建
if (this.panelCache && document.body.contains(this.panelCache)) {
this.updatePanelContent();
log('更新已有面板内容');
} else {
log('panelCache 不存在,等待 togglePanel 创建');
}
const t = translations[State.get('currentLanguage')] || translations.en;
const saveBtn = this.panelCache?.querySelector('#NiceFont_save-config');
if (saveBtn) {
saveBtn.textContent = `💾 ${State.get('isConfigModified') ? t.saveConfigPending : t.saveConfig}`;
}
}
}
};
// --- 多语言支持 ---
// 支持的多语言:汉语(zh)、英语(en)、韩语(ko)、日语(ja)、俄语(ru)、法语(fr)、德语(de)、西班牙语(es)、葡萄牙语(pt)
const translations = {
zh: {
increase: '增大字体',
decrease: '减小字体',
reset: '恢复字体',
fontSizeAdjustment: '字体大小调整',
setFontFamily: '字体类型调整',
setFontFamilyPrompt: '请输入字体类型',
supportFontFamily: '支持的字体类型:',
invalidFontFamilyAlert: '请输入有效的字体类型!',
firstAdjustmentConfirm: '请输入首次调整时间(秒,0表示禁用):',
firstAdjustmentEnabled: '首次调整字体: ✔️',
firstAdjustmentDisabled: '首次调整字体: ✖️',
timerPrompt: '请输入定时调整间隔(秒,0表示禁用):',
timerAdjustmentEnabled: '定时调整字体: ✔️',
timerAdjustmentDisabled: '定时调整字体: ✖️',
dynamicWatchConfirm: '是否启用/禁用动态调整?',
dynamicAdjustmentEnabled: '动态调整字体: ✔️',
dynamicAdjustmentDisabled: '动态调整字体: ✖️',
usageLanguage: '切换菜单语言',
switchPanel: '切换菜单面板',
tampermonkeyPanel: '油猴菜单',
floatingPanel: '页面菜单',
showPanel: '显示面板',
configScope: '配置作用范围',
subdomain: '子域名',
topLevelDomain: '顶级域名',
allWebsites: '所有网站',
configScopePrompt: '请输入配置作用范围:\n1: 子域名 ({hostname})\n2: 顶级域名 ({tld})\n3: 所有网站\n当前范围: {scope}',
invalidInput: '请输入有效的范围(1, 2, 或 3)!',
currentConfigScope: '当前配置源于',
notConfigured: '未配置',
saveConfig: '保存配置',
saveConfigPending: '保存配置(需确定)',
saveConfigConfirm: '确定保存配置到:{scope} [{target}]?',
deleteConfigConfirm: '确定删除当前配置吗?(将删除:{scope} [{target}])',
deleteBeforeScopeChangeConfirm: '更改为更广的作用范围需要先删除当前配置。\n确定删除当前配置吗?(将删除:{scope} [{target}])'
},
en: {
increase: 'Increase Font',
decrease: 'Decrease Font',
reset: 'Reset Font',
fontSizeAdjustment: 'Font Size Adjustment',
setFontFamily: 'Set Font Family',
setFontFamilyPrompt: 'Enter font family',
supportFontFamily: 'Supported font families:',
invalidFontFamilyAlert: 'Please enter a valid font family!',
firstAdjustmentConfirm: 'Enter first adjustment time (seconds, 0 to disable):',
firstAdjustmentEnabled: 'First Font Adjustment: ✔️',
firstAdjustmentDisabled: 'First Font Adjustment: ✖️',
timerPrompt: 'Enter timer adjustment interval (seconds, 0 to disable):',
timerAdjustmentEnabled: 'Timer Font Adjustment: ✔️',
timerAdjustmentDisabled: 'Timer Font Adjustment: ✖️',
dynamicWatchConfirm: 'Enable/Disable dynamic adjustment?',
dynamicAdjustmentEnabled: 'Dynamic Font Adjustment: ✔️',
dynamicAdjustmentDisabled: 'Dynamic Font Adjustment: ✖️',
usageLanguage: 'Switch Menu Language',
switchPanel: 'Switch Menu Panel',
tampermonkeyPanel: 'Tampermonkey Menu',
floatingPanel: 'Page Menu',
showPanel: 'Show Panel',
configScope: 'Config Scope',
subdomain: 'Subdomain',
topLevelDomain: 'Top-Level Domain',
allWebsites: 'All Websites',
configScopePrompt: 'Enter config scope:\n1: Subdomain ({hostname})\n2: Top-Level Domain ({tld})\n3: All Websites\nCurrent scope: {scope}',
invalidInput: 'Please enter a valid scope (1, 2, or 3)!',
currentConfigScope: 'Current Config Scope',
notConfigured: 'Not Configured',
saveConfig: 'Save Config',
saveConfigPending: 'Save Config (Pending)',
saveConfigConfirm: 'Save configuration to: {scope} [{target}]?',
deleteConfigConfirm: 'Are you sure to delete the current configuration? (Will delete: {scope} [{target}])',
deleteBeforeScopeChangeConfirm: 'Changing to a broader scope requires deleting the current configuration.\nAre you sure to delete the current configuration? (Will delete: {scope} [{target}])'
},
ko: {
increase: '글꼴 확대',
decrease: '글꼴 축소',
reset: '글꼴 초기화',
fontSizeAdjustment: '글꼴 크기 조정',
setFontFamily: '글꼴 설정',
setFontFamilyPrompt: '글꼴을 입력하세요',
supportFontFamily: '지원되는 글꼴:',
invalidFontFamilyAlert: '유효한 글꼴을 입력하세요!',
firstAdjustmentConfirm: '첫 조정 시간 입력 (초, 0은 비활성화):',
firstAdjustmentEnabled: '첫 글꼴 조정: ✔️',
firstAdjustmentDisabled: '첫 글꼴 조정: ✖️',
timerPrompt: '타이머 조정 간격 입력 (초, 0은 비활성화):',
timerAdjustmentEnabled: '타이머 글꼴 조정: ✔️',
timerAdjustmentDisabled: '타이머 글꼴 조정: ✖️',
dynamicWatchConfirm: '동적 조정을 활성화/비활성화 하시겠습니까?',
dynamicAdjustmentEnabled: '동적 글꼴 조정: ✔️',
dynamicAdjustmentDisabled: '동적 글꼴 조정: ✖️',
usageLanguage: '메뉴 언어 전환',
switchPanel: '메뉴 패널 전환',
tampermonkeyPanel: '탬퍼몽키 메뉴',
floatingPanel: '페이지 메뉴',
showPanel: '패널 표시',
configScope: '설정 범위',
subdomain: '서브도메인',
topLevelDomain: '최상위 도메인',
allWebsites: '모든 웹사이트',
configScopePrompt: '설정 범위를 입력하세요:\n1: 서브도메인 ({hostname})\n2: 최상위 도메인 ({tld})\n3: 모든 웹사이트\n현재 범위: {scope}',
invalidInput: '유효한 범위를 입력하세요 (1, 2, 또는 3)!',
currentConfigScope: '현재 설정 범위',
notConfigured: '설정되지 않음',
saveConfig: '설정 저장',
saveConfigPending: '설정 저장 (확인 필요)',
saveConfigConfirm: '설정을 다음에 저장하시겠습니까: {scope} [{target}]?',
deleteConfigConfirm: '현재 설정을 삭제하시겠습니까? (삭제될 항목: {scope} [{target}])',
deleteBeforeScopeChangeConfirm: '더 넓은 범위로 변경하려면 현재 설정을 삭제해야 합니다.\n현재 설정을 삭제하시겠습니까? (삭제될 항목: {scope} [{target}])'
},
ja: {
increase: 'フォントを大きくする',
decrease: 'フォントを小さくする',
reset: 'フォントをリセット',
fontSizeAdjustment: 'フォントサイズ調整',
setFontFamily: 'フォントファミリー設定',
setFontFamilyPrompt: 'フォントファミリーを入力してください',
supportFontFamily: 'サポートされているフォントファミリー:',
invalidFontFamilyAlert: '有効なフォントファミリーを入力してください!',
firstAdjustmentConfirm: '初回調整時間を入力してください(秒、0で無効):',
firstAdjustmentEnabled: '初回フォント調整:✔️',
firstAdjustmentDisabled: '初回フォント調整:✖️',
timerPrompt: 'タイマー調整間隔を入力してください(秒、0で無効):',
timerAdjustmentEnabled: 'タイマーフォント調整:✔️',
timerAdjustmentDisabled: 'タイマーフォント調整:✖️',
dynamicWatchConfirm: '動的調整を有効/無効にしますか?',
dynamicAdjustmentEnabled: '動的フォント調整:✔️',
dynamicAdjustmentDisabled: '動的フォント調整:✖️',
usageLanguage: 'メニュー言語の切り替え',
switchPanel: 'メニューパネルの切り替え',
tampermonkeyPanel: 'Tampermonkeyメニュー',
floatingPanel: 'ページメニュー',
showPanel: 'パネルを表示',
configScope: '設定範囲',
subdomain: 'サブドメイン',
topLevelDomain: 'トップレベルドメイン',
allWebsites: 'すべてのウェブサイト',
configScopePrompt: '設定範囲を入力してください:\n1: サブドメイン ({hostname})\n2: トップレベルドメイン ({tld})\n3: すべてのウェブサイト\n現在の範囲: {scope}',
invalidInput: '有効な範囲(1、2、または3)を入力してください!',
currentConfigScope: '現在の設定範囲',
notConfigured: '未設定',
saveConfig: '設定を保存',
saveConfigPending: '設定を保存(確認が必要)',
saveConfigConfirm: '設定を保存しますか:{scope} [{target}]?',
deleteConfigConfirm: '現在の設定を削除しますか?(削除対象:{scope} [{target}])',
deleteBeforeScopeChangeConfirm: 'より広い範囲に変更するには、現在の設定を削除する必要があります。\n現在の設定を削除しますか?(削除対象:{scope} [{target}])'
},
ru: {
increase: 'Увеличить шрифт',
decrease: 'Уменьшить шрифт',
reset: 'Сбросить шрифт',
fontSizeAdjustment: 'Регулировка размера шрифта',
setFontFamily: 'Установить семейство шрифтов',
setFontFamilyPrompt: 'Введите семейство шрифтов',
supportFontFamily: 'Поддерживаемые семейства шрифтов:',
invalidFontFamilyAlert: 'Пожалуйста, введите действительное семейство шрифтов!',
firstAdjustmentConfirm: 'Введите время первой настройки (секунды, 0 для отключения):',
firstAdjustmentEnabled: 'Первая настройка шрифта: ✔️',
firstAdjustmentDisabled: 'Первая настройка шрифта: ✖️',
timerPrompt: 'Введите интервал таймера настройки (секунды, 0 для отключения):',
timerAdjustmentEnabled: 'Настройка шрифта по таймеру: ✔️',
timerAdjustmentDisabled: 'Настройка шрифта по таймеру: ✖️',
dynamicWatchConfirm: 'Включить/отключить динамическую настройку?',
dynamicAdjustmentEnabled: 'Динамическая настройка шрифта: ✔️',
dynamicAdjustmentDisabled: 'Динамическая настройка шрифта: ✖️',
usageLanguage: 'Переключить язык меню',
switchPanel: 'Переключить панель меню',
tampermonkeyPanel: 'Меню Tampermonkey',
floatingPanel: 'Меню страницы',
showPanel: 'Показать панель',
configScope: 'Область конфигурации',
subdomain: 'Субдомен',
topLevelDomain: 'Домен верхнего уровня',
allWebsites: 'Все веб-сайты',
configScopePrompt: 'Введите область конфигурации:\n1: Субдомен ({hostname})\n2: Домен верхнего уровня ({tld})\n3: Все веб-сайты\nТекущая область: {scope}',
invalidInput: 'Пожалуйста, введите действительную область (1, 2 или 3)!',
currentConfigScope: 'Текущая область конфигурации',
notConfigured: 'Не настроено',
saveConfig: 'Сохранить конфигурацию',
saveConfigPending: 'Сохранить конфигурацию (ожидает подтверждения)',
saveConfigConfirm: 'Сохранить конфигурацию в: {scope} [{target}]?',
deleteConfigConfirm: 'Вы уверены, что хотите удалить текущую конфигурацию? (Будет удалено: {scope} [{target}])',
deleteBeforeScopeChangeConfirm: 'Для изменения на более широкую область необходимо удалить текущую конфигурацию.\nВы уверены, что хотите удалить текущую конфигурацию? (Будет удалено: {scope} [{target}])'
},
fr: {
increase: 'Augmenter la police',
decrease: 'Réduire la police',
reset: 'Réinitialiser la police',
fontSizeAdjustment: 'Ajustement de la taille de la police',
setFontFamily: 'Définir la famille de polices',
setFontFamilyPrompt: 'Entrez la famille de polices',
supportFontFamily: 'Familles de polices prises en charge :',
invalidFontFamilyAlert: 'Veuillez entrer une famille de polices valide !',
firstAdjustmentConfirm: 'Entrez le temps du premier ajustement (secondes, 0 pour désactiver) :',
firstAdjustmentEnabled: 'Premier ajustement de police : ✔️',
firstAdjustmentDisabled: 'Premier ajustement de police : ✖️',
timerPrompt: 'Entrez l’intervalle d’ajustement du minuteur (secondes, 0 pour désactiver) :',
timerAdjustmentEnabled: 'Ajustement de police par minuteur : ✔️',
timerAdjustmentDisabled: 'Ajustement de police par minuteur : ✖️',
dynamicWatchConfirm: 'Activer/désactiver l’ajustement dynamique ?',
dynamicAdjustmentEnabled: 'Ajustement dynamique de la police : ✔️',
dynamicAdjustmentDisabled: 'Ajustement dynamique de la police : ✖️',
usageLanguage: 'Changer la langue du menu',
switchPanel: 'Changer de panneau de menu',
tampermonkeyPanel: 'Menu Tampermonkey',
floatingPanel: 'Menu de la page',
showPanel: 'Afficher le panneau',
configScope: 'Portée de la configuration',
subdomain: 'Sous-domaine',
topLevelDomain: 'Domaine de premier niveau',
allWebsites: 'Tous les sites web',
configScopePrompt: 'Entrez la portée de la configuration :\n1 : Sous-domaine ({hostname})\n2 : Domaine de premier niveau ({tld})\n3 : Tous les sites web\nPortée actuelle : {scope}',
invalidInput: 'Veuillez entrer une portée valide (1, 2 ou 3) !',
currentConfigScope: 'Portée de configuration actuelle',
notConfigured: 'Non configuré',
saveConfig: 'Enregistrer la configuration',
saveConfigPending: 'Enregistrer la configuration (en attente)',
saveConfigConfirm: 'Enregistrer la configuration dans : {scope} [{target}] ?',
deleteConfigConfirm: 'Êtes-vous sûr de vouloir supprimer la configuration actuelle ? (Supprimera : {scope} [{target}])',
deleteBeforeScopeChangeConfirm: 'Changer pour une portée plus large nécessite de supprimer la configuration actuelle.\nÊtes-vous sûr de vouloir supprimer la configuration actuelle ? (Supprimera : {scope} [{target}])'
},
de: {
increase: 'Schriftart vergrößern',
decrease: 'Schriftart verkleinern',
reset: 'Schriftart zurücksetzen',
fontSizeAdjustment: 'Schriftgrößenanpassung',
setFontFamily: 'Schriftfamilie festlegen',
setFontFamilyPrompt: 'Geben Sie die Schriftfamilie ein',
supportFontFamily: 'Unterstützte Schriftfamilien:',
invalidFontFamilyAlert: 'Bitte geben Sie eine gültige Schriftfamilie ein!',
firstAdjustmentConfirm: 'Geben Sie die Zeit für die erste Anpassung ein (Sekunden, 0 zum Deaktivieren):',
firstAdjustmentEnabled: 'Erste Schrifteinstellung: ✔️',
firstAdjustmentDisabled: 'Erste Schrifteinstellung: ✖️',
timerPrompt: 'Geben Sie das Intervall für die Timer-Anpassung ein (Sekunden, 0 zum Deaktivieren):',
timerAdjustmentEnabled: 'Timer-Schrifteinstellung: ✔️',
timerAdjustmentDisabled: 'Timer-Schrifteinstellung: ✖️',
dynamicWatchConfirm: 'Dynamische Anpassung aktivieren/deaktivieren?',
dynamicAdjustmentEnabled: 'Dynamische Schrifteinstellung: ✔️',
dynamicAdjustmentDisabled: 'Dynamische Schrifteinstellung: ✖️',
usageLanguage: 'Menüsprache wechseln',
switchPanel: 'Menüpanel wechseln',
tampermonkeyPanel: 'Tampermonkey-Menü',
floatingPanel: 'Seitenmenü',
showPanel: 'Panel anzeigen',
configScope: 'Konfigurationsbereich',
subdomain: 'Subdomain',
topLevelDomain: 'Top-Level-Domain',
allWebsites: 'Alle Websites',
configScopePrompt: 'Geben Sie den Konfigurationsbereich ein:\n1: Subdomain ({hostname})\n2: Top-Level-Domain ({tld})\n3: Alle Websites\nAktueller Bereich: {scope}',
invalidInput: 'Bitte geben Sie einen gültigen Bereich ein (1, 2 oder 3)!',
currentConfigScope: 'Aktueller Konfigurationsbereich',
notConfigured: 'Nicht konfiguriert',
saveConfig: 'Konfiguration speichern',
saveConfigPending: 'Konfiguration speichern (ausstehend)',
saveConfigConfirm: 'Konfiguration speichern in: {scope} [{target}]?',
deleteConfigConfirm: 'Möchten Sie die aktuelle Konfiguration wirklich löschen? (Wird gelöscht: {scope} [{target}])',
deleteBeforeScopeChangeConfirm: 'Zum Wechseln zu einem breiteren Bereich muss die aktuelle Konfiguration gelöscht werden.\nMöchten Sie die aktuelle Konfiguration wirklich löschen? (Wird gelöscht: {scope} [{target}])'
},
es: {
increase: 'Aumentar fuente',
decrease: 'Reducir fuente',
reset: 'Restablecer fuente',
fontSizeAdjustment: 'Ajuste del tamaño de fuente',
setFontFamily: 'Establecer familia de fuentes',
setFontFamilyPrompt: 'Ingrese la familia de fuentes',
supportFontFamily: 'Familias de fuentes compatibles:',
invalidFontFamilyAlert: '¡Por favor, ingrese una familia de fuentes válida!',
firstAdjustmentConfirm: 'Ingrese el tiempo del primer ajuste (segundos, 0 para desactivar):',
firstAdjustmentEnabled: 'Primer ajuste de fuente: ✔️',
firstAdjustmentDisabled: 'Primer ajuste de fuente: ✖️',
timerPrompt: 'Ingrese el intervalo de ajuste del temporizador (segundos, 0 para desactivar):',
timerAdjustmentEnabled: 'Ajuste de fuente por temporizador: ✔️',
timerAdjustmentDisabled: 'Ajuste de fuente por temporizador: ✖️',
dynamicWatchConfirm: '¿Activar/desactivar el ajuste dinámico?',
dynamicAdjustmentEnabled: 'Ajuste dinámico de fuente: ✔️',
dynamicAdjustmentDisabled: 'Ajuste dinámico de fuente: ✖️',
usageLanguage: 'Cambiar idioma del menú',
switchPanel: 'Cambiar panel del menú',
tampermonkeyPanel: 'Menú de Tampermonkey',
floatingPanel: 'Menú de página',
showPanel: 'Mostrar panel',
configScope: 'Alcance de la configuración',
subdomain: 'Subdominio',
topLevelDomain: 'Dominio de nivel superior',
allWebsites: 'Todos los sitios web',
configScopePrompt: 'Ingrese el alcance de la configuración:\n1: Subdominio ({hostname})\n2: Dominio de nivel superior ({tld})\n3: Todos los sitios web\nAlcance actual: {scope}',
invalidInput: '¡Por favor, ingrese un alcance válido (1, 2 o 3)!',
currentConfigScope: 'Alcance de configuración actual',
notConfigured: 'No configurado',
saveConfig: 'Guardar configuración',
saveConfigPending: 'Guardar configuración (pendiente)',
saveConfigConfirm: '¿Guardar configuración en: {scope} [{target}]?',
deleteConfigConfirm: '¿Está seguro de que desea eliminar la configuración actual? (Se eliminará: {scope} [{target}])',
deleteBeforeScopeChangeConfirm: 'Cambiar a un alcance más amplio requiere eliminar la configuración actual.\n¿Está seguro de que desea eliminar la configuración actual? (Se eliminará: {scope} [{target}])'
},
pt: {
increase: 'Aumentar fonte',
decrease: 'Diminuir fonte',
reset: 'Redefinir fonte',
fontSizeAdjustment: 'Ajuste do tamanho da fonte',
setFontFamily: 'Definir família de fontes',
setFontFamilyPrompt: 'Digite a família de fontes',
supportFontFamily: 'Famílias de fontes suportadas:',
invalidFontFamilyAlert: 'Por favor, insira uma família de fontes válida!',
firstAdjustmentConfirm: 'Digite o tempo do primeiro ajuste (segundos, 0 para desativar):',
firstAdjustmentEnabled: 'Primeiro ajuste de fonte: ✔️',
firstAdjustmentDisabled: 'Primeiro ajuste de fonte: ✖️',
timerPrompt: 'Digite o intervalo de ajuste do temporizador (segundos, 0 para desativar):',
timerAdjustmentEnabled: 'Ajuste de fonte por temporizador: ✔️',
timerAdjustmentDisabled: 'Ajuste de fonte por temporizador: ✖️',
dynamicWatchConfirm: 'Ativar/desativar ajuste dinâmico?',
dynamicAdjustmentEnabled: 'Ajuste dinâmico de fonte: ✔️',
dynamicAdjustmentDisabled: 'Ajuste dinâmico de fonte: ✖️',
usageLanguage: 'Mudar idioma do menu',
switchPanel: 'Mudar painel do menu',
tampermonkeyPanel: 'Menu Tampermonkey',
floatingPanel: 'Menu da página',
showPanel: 'Mostrar painel',
configScope: 'Escopo da configuração',
subdomain: 'Subdomínio',
topLevelDomain: 'Domínio de nível superior',
allWebsites: 'Todos os sites',
configScopePrompt: 'Digite o escopo da configuração:\n1: Subdomínio ({hostname})\n2: Domínio de nível superior ({tld})\n3: Todos os sites\nEscopo atual: {scope}',
invalidInput: 'Por favor, insira um escopo válido (1, 2 ou 3)!',
currentConfigScope: 'Escopo de configuração atual',
notConfigured: 'Não configurado',
saveConfig: 'Salvar configuração',
saveConfigPending: 'Salvar configuração (pendente)',
saveConfigConfirm: 'Salvar configuração em: {scope} [{target}]?',
deleteConfigConfirm: 'Tem certeza de que deseja excluir a configuração atual? (Será excluído: {scope} [{target}])',
deleteBeforeScopeChangeConfirm: 'Mudar para um escopo mais amplo exige a exclusão da configuração atual.\nTem certeza de que deseja excluir a configuração atual? (Será excluído: {scope} [{target}])'
}
};
// --- CSS 样式 ---
GM_addStyle(`
#NiceFont_panel {
position: fixed;
width: 300px;
background: #fff;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 10001;
font-family: sans-serif !important;
font-size: 15px;
user-select: none;
}
#NiceFont_panel .NiceFont_header {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 10002;
}
#NiceFont_panel .NiceFont_header > div {
font-size: 16px !important;
font-family: sans-serif !important;
text-align: left;
flex-grow: 1;
cursor: grab;
margin: 5px;
font-weight: bold;
}
#NiceFont_panel .NiceFont_close-btn {
border: none;
border-radius: 3px;
padding: 1px 6px;
cursor: pointer;
line-height: 16px;
font-size: 12px;
background: none;
color: #000;
}
#NiceFont_panel .NiceFont_close-btn:hover {
text-decoration: underline;
}
#NiceFont_panel .action-btn {
display: block;
padding: 2px;
border-radius: 3px;
cursor: pointer;
text-align: left;
}
#NiceFont_panel .action-btn:hover {
text-decoration: underline;
}
#NiceFont_panel #NiceFont_set-font-size-btn {
padding: 2px;
text-decoration: none !important;
}
#NiceFont_panel .font-family-select {
display: inline-block;
width: auto;
padding: 2px;
margin-left: 5px;
border: 1px solid #ddd;
border-radius: 3px;
font-size: 14px;
vertical-align: middle;
}
`);
// --- 初始化 ---
/**
* 初始化脚本
*/
function init() {
// 检查旧版本存储数据并清理(用于升级兼容)
const oldVersion = GM_getValue('NiceFont_version', '0.0');
const currentVersion = GM_info?.script?.version;
if (oldVersion !== currentVersion) {
// 清理可能导致冲突的旧存储
GM_setValue('NiceFont_autoOpenDisabled', false);
GM_setValue('NiceFont_closeCount', 0);
GM_setValue('NiceFont_lastCloseTime', 0);
GM_setValue('NiceFont_autoShowAfterLanguageSwitch', true); // 默认启用自动显示
GM_setValue('NiceFont_version', currentVersion);
log(`检测到版本升级: ${oldVersion} -> ${currentVersion},清理旧存储数据`);
}
// 初始化语言
let lang = GM_getValue('language', navigator.language);
if (!translations[lang]) {
lang = lang.startsWith('zh') ? 'zh' : 'en';
GM_setValue('language', lang);
}
State.set('currentLanguage', lang);
log(`语言设置为: ${lang}`);
// 初始化面板类型
let panelType = GM_getValue(ConfigScopeManager.PANEL_TYPE_KEY, 'floating');
State.set('panelType', panelType);
log(`面板类型设置为: ${panelType}`);
// 加载配置
ConfigManager.loadConfig();
// 初始化界面
UIManager.updateUI();
// 延迟创建浮动面板,直到 DOM 就绪
function initializePanel() {
if (State.get('panelType') === 'floating' && (!UIManager.panelCache || !document.body.contains(UIManager.panelCache))) {
UIManager.createFloatingPanel();
if (UIManager.panelCache) {
UIManager.panelCache.style.display = 'none';
UIManager.overlayCache.style.display = 'none';
log('首次初始化浮动面板,设置为隐藏状态');
} else {
console.error('[NiceFont] 初始创建浮动面板失败');
}
}
}
if (document.body) {
initializePanel();
} else {
document.addEventListener('DOMContentLoaded', initializePanel, { once: true });
log('document.body 未就绪,延迟面板初始化至 DOMContentLoaded');
}
// 初始化字体调整
window.addEventListener('load', () => {
if (State.get('currentAdjustment') !== 0 || State.get('currentFontFamily') !== 'none') {
if (State.get('firstAdjustment') && State.get('firstAdjustmentTime') > 0) {
setTimeout(() => {
FontManager.applyFontRecursively(document.body, State.get('currentAdjustment'));
log('应用首次字体调整');
}, State.get('firstAdjustmentTime') * 1000);
}
if (State.get('watchDOMChanges')) {
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('intervalSeconds') > 0) {
if (State.get('observer')) State.get('observer').disconnect();
State.set('timer', setInterval(() => {
FontManager.applyFontRecursively(document.body, State.get('currentAdjustment'));
}, State.get('intervalSeconds') * 1000));
log(`定时调整启用: ${State.get('intervalSeconds')}s`);
}
}
});
log('脚本初始化完成');
}
init();
})();