// ==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})
${Object.entries(this.state.localFonts).map(([name, font]) => `
-
${name}
`).join('')}
`;
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);
})();