// ==UserScript== // @name 移动端网页字体修改器 // @namespace http://via-browser.com/ // @version 1.4 // @description 支持字体管理和拖动按钮的移动端字体工具 // @author ^_^ // @match *://*/* // @grant none // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 配置参数 const CONFIG = { MAX_FILE_SIZE: 5 * 1024 * 1024, // 5MB DEFAULT_FONT: 'system-ui', STORAGE_KEY: 'VIA_FONT_SETTINGS', FONT_TYPES: { 'ttf': { format: 'truetype' }, 'otf': { format: 'opentype' }, 'woff': { format: 'woff' }, 'woff2':{ format: 'woff2' } } }; // 存储管理 const storage = { get() { return JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEY)) || { currentFont: CONFIG.DEFAULT_FONT, localFonts: {}, enabled: true, fabPosition: null }; }, save(data) { localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(data)); } }; // 移动端UI组件 class MobileUI { static createFAB() { const state = storage.get(); const fab = document.createElement('div'); fab.id = 'via-font-fab'; fab.innerHTML = 'A'; Object.assign(fab.style, { position: 'fixed', width: '50px', height: '50px', background: '#2196F3', color: 'white', borderRadius: '50%', textAlign: 'center', lineHeight: '50px', fontSize: '24px', boxShadow: '0 4px 8px rgba(0,0,0,0.3)', zIndex: 999999, touchAction: 'none', userSelect: 'none', left: state.fabPosition ? `${state.fabPosition.x}px` : 'auto', top: state.fabPosition ? `${state.fabPosition.y}px` : 'auto', right: state.fabPosition ? 'auto' : '20px', bottom: state.fabPosition ? 'auto' : '30px', transition: 'left 0.2s, top 0.2s' }); document.body.appendChild(fab); return fab; } static createPanel() { const panel = document.createElement('div'); panel.id = 'via-font-panel'; Object.assign(panel.style, { position: 'fixed', bottom: '0', left: '0', right: '0', background: 'white', padding: '16px', boxShadow: '0 -4px 10px rgba(0,0,0,0.1)', transform: 'translateY(100%)', transition: 'transform 0.3s ease', maxHeight: '70vh', overflowY: 'auto', zIndex: 999998 }); panel.innerHTML = `

字体设置

`; document.body.appendChild(panel); return panel; } } // 字体管理器 class FontManager { constructor() { this.state = storage.get(); this.isDragging = false; this.initUI(); this.setupDrag(); this.applyCurrentFont(); } initUI() { this.fab = MobileUI.createFAB(); this.panel = MobileUI.createPanel(); // 关闭按钮事件 this.panel.querySelector('.close-btn').addEventListener('click', () => { this.hidePanel(); }); // 悬浮按钮点击事件 this.fab.addEventListener('click', (e) => { if (this.isDragging) { e.preventDefault(); e.stopPropagation(); this.isDragging = false; return; } this.togglePanel(); }); } setupDrag() { let startX, startY, initialX, initialY; const onTouchStart = (e) => { this.isDragging = false; const touch = e.touches[0]; startX = touch.clientX; startY = touch.clientY; initialX = this.fab.offsetLeft; initialY = this.fab.offsetTop; this.fab.style.transition = 'none'; document.addEventListener('touchmove', onTouchMove); document.addEventListener('touchend', onTouchEnd); }; const onTouchMove = (e) => { if (!e.touches.length) return; const touch = e.touches[0]; const deltaX = touch.clientX - startX; const deltaY = touch.clientY - startY; if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { this.isDragging = true; } let newX = initialX + deltaX; let newY = initialY + deltaY; const maxX = window.innerWidth - this.fab.offsetWidth; const maxY = window.innerHeight - this.fab.offsetHeight; newX = Math.max(0, Math.min(newX, maxX)); newY = Math.max(0, Math.min(newY, maxY)); this.fab.style.left = `${newX}px`; this.fab.style.top = `${newY}px`; this.fab.style.right = 'auto'; this.fab.style.bottom = 'auto'; }; const onTouchEnd = () => { document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); this.fab.style.transition = 'left 0.2s, top 0.2s'; if (this.isDragging) { this.state.fabPosition = { x: this.fab.offsetLeft, y: this.fab.offsetTop }; storage.save(this.state); } }; this.fab.addEventListener('touchstart', onTouchStart); } togglePanel() { const isOpen = this.panel.style.transform === 'translateY(0%)'; this.panel.style.transform = isOpen ? 'translateY(100%)' : 'translateY(0%)'; if (!isOpen) this.refreshPanel(); } refreshPanel() { const content = this.panel.querySelector('.content'); content.innerHTML = `

已安装字体 (${Object.keys(this.state.localFonts).length})

`; content.querySelector('.font-select').addEventListener('change', e => { this.applyFont(e.target.value); }); content.querySelector('input[type="checkbox"]').addEventListener('change', e => { this.toggleFeature(e.target.checked); }); content.querySelector('#font-upload').addEventListener('change', async e => { await this.processFiles(Array.from(e.target.files)); this.refreshPanel(); }); content.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', () => { const fontName = btn.dataset.font; this.deleteFont(fontName); }); }); } async processFiles(files) { for (const file of files) { await this.handleFontFile(file); } } async handleFontFile(file) { if (file.size > CONFIG.MAX_FILE_SIZE) { alert(`文件大小超过限制 (最大${CONFIG.MAX_FILE_SIZE/1024/1024}MB)`); return; } const ext = file.name.split('.').pop().toLowerCase(); if (!CONFIG.FONT_TYPES[ext]) { alert(`不支持的文件类型: ${ext}`); return; } const fontName = prompt('请输入字体名称:', file.name.replace(/\.[^.]+$/, '')); if (!fontName) return; if (this.state.localFonts[fontName] && !confirm('字体已存在,是否覆盖?')) return; const dataURL = await this.readFileAsDataURL(file); this.state.localFonts[fontName] = { name: fontName, data: dataURL, format: CONFIG.FONT_TYPES[ext].format }; storage.save(this.state); } readFileAsDataURL(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); } deleteFont(fontName) { if (!confirm(`确定删除字体 "${fontName}" 吗?`)) return; delete this.state.localFonts[fontName]; if (this.state.currentFont === fontName) { this.applyFont(CONFIG.DEFAULT_FONT); } storage.save(this.state); this.refreshPanel(); } applyFont(fontName) { document.querySelectorAll('style[data-custom-font]').forEach(e => e.remove()); if (fontName !== CONFIG.DEFAULT_FONT) { const fontData = this.state.localFonts[fontName]; if (fontData) { const style = document.createElement('style'); style.dataset.customFont = fontName; style.textContent = ` @font-face { font-family: "${fontName}"; src: url(${fontData.data}) format("${fontData.format}"); font-display: swap; } body *:not(input):not(textarea) { font-family: "${fontName}" !important; } `; document.head.appendChild(style); // 字体加载检测 const checkFont = () => { if (document.fonts.check(`12px "${fontName}"`)) { document.body.style.fontFamily = `${fontName}, sans-serif`; } else { document.fonts.load(`12px "${fontName}"`).then(checkFont); } }; checkFont(); } } this.state.currentFont = fontName; storage.save(this.state); } toggleFeature(enabled) { this.state.enabled = enabled; storage.save(this.state); document.body.style.fontFamily = enabled ? `${this.state.currentFont}, sans-serif` : 'inherit'; } hidePanel() { this.panel.style.transform = 'translateY(100%)'; } applyCurrentFont() { if (this.state.enabled) { this.applyFont(this.state.currentFont); } } } // 初始化 setTimeout(() => new FontManager(), 1000); })();