Greasy Fork

Greasy Fork is available in English.

移动端网页字体修改器

支持字体和颜色管理的移动端字体工具

当前为 2025-06-01 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
});