// ==UserScript==
// @name 移动端网页字体修改器
// @namespace http://via-browser.com/
// @version 2.3
// @description 支持字体和颜色管理的移动端字体工具
// @author ^o^
// @match *://*/*
// @run-at document-start
// @grant GM_registerMenuCommand
// ==/UserScript==
// 配置参数
const CONFIG = {
DB_NAME: 'VIA_FONT_DB',
FONT_TYPES: {
'ttf': { format: 'truetype' },
'otf': { format: 'opentype' },
'woff': { format: 'woff' },
'woff2':{ format: 'woff2' }
}
};
// IndexedDB存储管理
class DatabaseManager {
constructor() {
this.db = null;
this.dbName = CONFIG.DB_NAME;
this.storeName = 'fontStore';
}
init() {
return new Promise((resolve, reject) => {
const request = window.indexedDB.open(this.dbName, 1);
request.onupgradeneeded = function(event) {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, { keyPath: 'id' });
}
}.bind(this);
request.onsuccess = function(event) {
this.db = event.target.result;
resolve();
}.bind(this);
request.onerror = function(event) {
reject(event.target.error);
};
});
}
getSettings() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get('settings');
request.onsuccess = function() {
resolve(request.result || {
currentFont: CONFIG.DEFAULT_FONT,
fontColor: CONFIG.DEFAULT_FONT_COLOR
});
};
request.onerror = function() {
reject(request.error);
};
});
}
saveSettings(data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put({
id: 'settings',
...data
});
request.onsuccess = function() {
resolve();
};
request.onerror = function() {
reject(request.error);
};
});
}
getFonts() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = function() {
resolve(request.result.filter(item => item.id !== 'settings'));
};
request.onerror = function() {
reject(request.error);
};
});
}
saveFont(fontData) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.add(fontData);
request.onsuccess = function() {
resolve();
};
request.onerror = function() {
if (request.error.name === 'ConstraintError') {
const updateRequest = store.put(fontData);
updateRequest.onsuccess = function() {
resolve();
};
updateRequest.onerror = function() {
reject(updateRequest.error);
};
} else {
reject(request.error);
}
};
});
}
deleteFont(fontName) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.delete(fontName);
request.onsuccess = function() {
resolve();
};
request.onerror = function() {
reject(request.error);
};
});
}
}
// 在脚本管理器中显示“打开字体设置”选项
let fontManagerInstance = null; // 存储 FontManager 实例
function openFontSettings() {
if (fontManagerInstance) {
fontManagerInstance.togglePanel();
}
}
// 注册菜单命令
GM_registerMenuCommand('打开字体设置', openFontSettings);
// 字体管理器
class FontManager {
constructor() {
this.dbManager = new DatabaseManager();
this.state = {
currentFont: CONFIG.DEFAULT_FONT,
fontColor: CONFIG.DEFAULT_FONT_COLOR,
localFonts: []
};
this.init().then(() => {
this.loadFonts().then(() => {
this.applyCurrentSettings();
this.preloadFonts();
this.createPanel();
this.refreshPanel();
});
}).catch(error => {
console.error('Initialization failed:', error);
});
}
async init() {
await this.dbManager.init();
}
createPanel() {
this.panel = document.createElement('div');
this.panel.id = 'via-font-panel';
Object.assign(this.panel.style, {
position: 'fixed',
bottom: '0',
left: '0',
right: '0',
background: 'rgba(255, 255, 255, 0.85)',
backdropFilter: 'blur(15px)',
borderRadius: '16px 16px 0 0',
boxShadow: '0 -8px 20px rgba(0,0,0,0.12)',
padding: '20px',
transform: 'translateY(100%)',
transition: 'transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
maxHeight: '80vh',
overflowY: 'auto',
zIndex: 999998,
WebkitBackdropFilter: 'blur(15px)',
display: 'none'
});
this.panel.innerHTML = `
<div class="header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; padding-bottom:16px; border-bottom:1px solid rgba(233, 236, 239, 0.5)">
<h3 style="margin:0; font-family:'Segoe UI', 'Roboto', sans-serif; font-weight:600; color:#343a40">自定义字体管理</h3>
<button class="close-btn" style="background:none; border:none; font-size:20px; cursor:pointer; color:#adb5bd; transition:color 0.2s">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="#adb5bd" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<div class="content" style="padding-top:10px"></div>
`;
document.body.appendChild(this.panel);
}
togglePanel() {
if (this.panel.style.display === 'none' || this.panel.style.display === '') {
this.panel.style.display = 'block';
this.panel.style.transform = 'translateY(0%)';
} else {
this.panel.style.transform = 'translateY(100%)';
this.panel.style.display = 'none';
}
}
refreshPanel() {
const content = this.panel.querySelector('.content');
content.innerHTML = `
<div style="margin-bottom:20px; padding:15px; background:rgba(241, 245, 249, 0.7); border-radius:10px; border:1px solid rgba(224, 231, 255, 0.5); backdrop-filter:blur(5px)">
<select class="font-select" style="width:100%; padding:10px 15px; border-radius:6px; border:1px solid rgba(209, 213, 219, 0.8); font-family:'Noto Sans SC', sans-serif; font-size:15px; box-shadow:inset 0 2px 4px rgba(0,0,0,0.05); transition:box-shadow 0.2s; background:rgba(255,255,255,0.9)">
<option value="system-ui">系统默认字体</option>
${this.state.localFonts.map(font => `
<option value="${font.name}" ${this.state.currentFont === font.name ? 'selected' : ''}>
${font.name}
</option>
`).join('')}
</select>
</div>
<div style="margin-bottom:20px; padding:15px; background:rgba(241, 245, 249, 0.7); border-radius:10px; border:1px solid rgba(224, 232, 255, 0.5); backdrop-filter:blur(5px)">
<label style="display:block; margin-bottom:10px; font-family:'Noto Sans SC', sans-serif; font-size:15px; color:#4a5568">字体颜色</label>
<div style="display:flex; align-items:center; gap:15px">
<input type="color" id="font-color-picker" value="${this.state.fontColor}" style="width:40px; height:40px; border-radius:6px; border:none; cursor:pointer">
<div style="display:flex; flex-direction:column; gap:10px; flex-grow:1">
<input type="text" id="font-color-code" value="${this.state.fontColor}" placeholder="#RRGGBB" style="width:100%; padding:8px 12px; border-radius:6px; border:1px solid rgba(209, 213, 219, 0.8); font-family:'Noto Sans SC', sans-serif; font-size:14px; box-shadow:inset 0 2px 4px rgba(0,0,0,0.05); transition:box-shadow 0.2s; background:rgba(255,255,255,0.9)">
<button id="color-confirm-btn" style="width:100%; padding:8px 12px; background:#4299e1; color:white; border:none; border-radius:6px; font-family:'Noto Sans SC', sans-serif; font-size:14px; font-weight:500; cursor:pointer; transition:background 0.2s">
确认
</button>
</div>
</div>
</div>
<div style="margin:20px 0; padding:15px; background:rgba(248, 249, 250, 0.7); border-radius:10px; border:1px solid rgba(226, 232, 240, 0.5); backdrop-filter:blur(5px)">
<label style="display:block; margin-bottom:10px; font-family:'Noto Sans SC', sans-serif; font-size:15px; color:#4a5568">批量上传字体文件(支持 ${Object.keys(CONFIG.FONT_TYPES).map(e => `.${e}`).join(', ')})</label>
<input type="file" accept="${Object.keys(CONFIG.FONT_TYPES).map(e => `.${e}`).join(',')}"
multiple style="width:100%; padding:10px 15px; border-radius:6px; border:2px dashed rgba(160, 174, 192, 0.5); font-family:'Noto Sans SC', sans-serif; cursor:pointer; transition:border 0.2s; background:rgba(255,255,255,0.7)">
</div>
<div class="font-list" style="margin-top:20px">
<h4 style="margin-bottom:10px; font-family:'Noto Sans SC', sans-serif; font-size:17px; color:#2d3748; font-weight:600; background:rgba(241, 245, 249, 0.5); padding:8px 15px; border-radius:8px; backdrop-filter:blur(5px)">已安装字体 (${this.state.localFonts.length})</h4>
<ul style="list-style:none; padding:0; margin:0; display:grid; grid-template-columns:repeat(auto-fill, minmax(180px, 1fr)); gap:15px">
${this.state.localFonts.map(font => `
<li style="background:rgba(241, 245, 249, 0.7); padding:15px; border-radius:8px; display:flex; flex-direction:column; gap:10px; border:1px solid rgba(226, 232, 240, 0.5); box-shadow:0 2px 4px rgba(0,0,0,0.05); transition:transform 0.2s, box-shadow 0.2s; backdrop-filter:blur(5px)">
<span style="font-family:'Noto Sans SC', sans-serif; font-size:15px; color:#4a5568">${font.name}</span>
<button data-font="${font.name}" class="delete-btn"
style="padding:6px 12px; background:#ef233c; color:white; border:none; border-radius:5px; font-family:'Noto Sans SC', sans-serif; font-size:14px; font-weight:500; cursor:pointer; transition:background 0.2s; justify-self:flex-end">
删除
</button>
</li>
`).join('')}
</ul>
</div>
`;
this.setupPanelEventListeners();
}
setupPanelEventListeners() {
this.panel.querySelector('.font-select').addEventListener('change', e => {
this.applyFont(e.target.value);
});
this.panel.querySelector('#font-color-picker').addEventListener('input', e => {
this.state.fontColor = e.target.value;
document.documentElement.style.setProperty('--global-font-color', this.state.fontColor);
this.panel.querySelector('#font-color-code').value = this.state.fontColor;
this.saveSettings();
});
this.panel.querySelector('#font-color-code').addEventListener('input', e => {
if (/^#[0-9A-Fa-f]{6}$/.test(e.target.value)) {
this.state.fontColor = e.target.value;
document.documentElement.style.setProperty('--global-font-color', this.state.fontColor);
this.panel.querySelector('#font-color-picker').value = this.state.fontColor;
}
});
this.panel.querySelector('#color-confirm-btn').addEventListener('click', () => {
const colorCode = this.panel.querySelector('#font-color-code').value;
if (/^#[0-9A-Fa-f]{6}$/.test(colorCode)) {
this.state.fontColor = colorCode;
document.documentElement.style.setProperty('--global-font-color', this.state.fontColor);
this.panel.querySelector('#font-color-picker').value = this.state.fontColor;
this.saveSettings();
} else {
alert('请输入有效的颜色代码(如 #FF5733)');
}
});
this.panel.querySelector('input[type="file"]').addEventListener('change', async e => {
await this.handleFiles(Array.from(e.target.files));
this.refreshPanel();
});
this.panel.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.deleteFont(btn.dataset.font);
});
});
this.panel.querySelector('.close-btn').addEventListener('click', () => {
this.togglePanel();
});
// 添加全局样式
const style = document.createElement('style');
style.textContent = `
:root {
--primary-color: #4299e1;
--primary-dark: #3182ce;
--text-color: #2d3748;
--bg-color: #f8f9fa;
--border-color: #e2e8f0;
--global-font-color: ${this.state.fontColor};
}
body *:not(input):not(textarea) {
color: var(--global-font-color) !important;
}
.font-select:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.3);
border-color: var(--primary-dark);
}
input[type="file"]:hover {
border-color: rgba(160, 174, 192, 0.8);
background: rgba(241, 245, 249, 0.8);
}
input[type="file"]:active {
border-color: rgba(73, 85, 102, 0.8);
background: rgba(226, 232, 240, 0.8);
}
.delete-btn:hover {
background: #e53e3e;
}
.close-btn:hover svg {
color: rgba(113, 128, 150, 0.8);
}
.panel {
overflow-y: auto !important;
}
#font-color-picker::-webkit-color-swatch-wrapper {
padding: 0;
}
#font-color-picker::-webkit-color-swatch {
border: none;
border-radius: 6px;
}
#color-confirm-btn:hover {
background: #3182ce;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(226, 232, 240, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(160, 174, 192, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(160, 174, 192, 0.8);
}
body {
--webkit-backdrop-filter: blur(15px);
}
`;
document.head.appendChild(style);
}
async handleFiles(files) {
for (const file of files) {
await this.handleFontFile(file);
}
}
async handleFontFile(file) {
const ext = file.name.split('.').pop().toLowerCase();
if (!CONFIG.FONT_TYPES[ext]) {
alert(`不支持的文件类型: ${ext}`);
return;
}
const fontName = prompt('请输入字体名称:', file.name.replace(/\.[^.]+$/, ''));
if (!fontName) return;
let fontExists = this.state.localFonts.some(font => font.name === fontName);
if (fontExists && !confirm('字体已存在,是否覆盖?')) return;
const dataURL = await this.readFileAsDataURL(file);
const fontData = {
id: fontName,
name: fontName,
data: dataURL,
format: CONFIG.FONT_TYPES[ext].format,
date: new Date().toISOString()
};
await this.dbManager.saveFont(fontData);
// 更新状态和UI
this.state.localFonts = await this.dbManager.getFonts();
if (fontExists) {
this.applyFont(fontName);
}
this.refreshPanel();
}
readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
async deleteFont(fontName) {
if (!confirm(`确定删除字体 "${fontName}" 吗?`)) return;
await this.dbManager.deleteFont(fontName);
// 更新状态和UI
this.state.localFonts = await this.dbManager.getFonts();
if (this.state.currentFont === fontName) {
this.applyFont(CONFIG.DEFAULT_FONT);
}
this.refreshPanel();
}
async applyFont(fontName) {
// 移除旧的字体样式
document.querySelectorAll('style[data-custom-font]').forEach(e => e.remove());
// 应用新的字体
if (fontName !== CONFIG.DEFAULT_FONT) {
const font = this.state.localFonts.find(f => f.name === fontName);
if (font) {
const style = document.createElement('style');
style.dataset.customFont = fontName;
style.textContent = `
@font-face {
font-family: "${fontName}";
src: url(${font.data}) format("${font.format}");
font-display: swap;
}
body *:not(input):not(textarea) {
font-family: "${fontName}" !important;
}
`;
document.head.appendChild(style);
}
}
this.state.currentFont = fontName;
await this.saveSettings();
this.panel.querySelectorAll('.font-select option').forEach(option => {
if (option.value === fontName) {
option.selected = true;
}
});
}
async loadFonts() {
try {
const settings = await this.dbManager.getSettings();
this.state.currentFont = settings.currentFont || CONFIG.DEFAULT_FONT;
this.state.fontColor = settings.fontColor || CONFIG.DEFAULT_FONT_COLOR;
} catch (error) {
console.error('Failed to load settings:', error);
}
try {
this.state.localFonts = await this.dbManager.getFonts();
} catch (error) {
console.error('Failed to load fonts:', error);
}
}
async saveSettings() {
await this.dbManager.saveSettings({
currentFont: this.state.currentFont,
fontColor: this.state.fontColor
});
}
async applyCurrentSettings() {
await this.applyFont(this.state.currentFont);
document.documentElement.style.setProperty('--global-font-color', this.state.fontColor);
document.querySelectorAll('body *:not(input):not(textarea)').forEach(el => {
el.style.color = this.state.fontColor;
});
}
preloadFonts() {
this.state.localFonts.forEach(font => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'font';
link.type = `font/${font.format}`;
link.crossOrigin = 'anonymous';
link.href = font.data;
link.onload = () => {
console.log(`Font ${font.name} preloaded`);
};
link.onerror = () => {
console.error(`Failed to preload font ${font.name}`);
};
document.head.appendChild(link);
});
}
}
// 初始化 FontManager
document.addEventListener('DOMContentLoaded', () => {
fontManagerInstance = new FontManager();
});