Linux.do Base64 script

Base64编解码工具,支持位置记忆、夜间模式和拖动功能

Version au 01/04/2025. Voir la dernière version.

// ==UserScript==
// @name         Linux.do Base64 script
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Base64编解码工具,支持位置记忆、夜间模式和拖动功能
// @author       Xavier
// @match        https://linux.do/*
// @grant        GM_setClipboard
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==


(function() {
    'use strict';

    // 创建主容器
    const container = document.createElement('div');
    container.id = 'b64Container';
    Object.assign(container.style, {
        position: 'fixed',
        zIndex: 9999,
        borderRadius: '8px',
        fontFamily: 'inherit',
        cursor: 'move'
    });

    // 初始化位置
    const savedPosition = GM_getValue('b64Position', null);
    if (savedPosition) {
        container.style.left = `${savedPosition.x}px`;
        container.style.top = `${savedPosition.y}px`;
        container.style.bottom = 'auto';
        container.style.right = 'auto';
    } else {
        container.style.bottom = '20px';
        container.style.right = '20px';
        container.style.left = 'auto';
        container.style.top = 'auto';
    }

    // 主触发器
    const trigger = document.createElement('div');
    trigger.innerHTML = 'Base64';
    Object.assign(trigger.style, {
        padding: '8px 16px',
        borderRadius: '6px',
        fontSize: '14px',
        cursor: 'pointer',
        transition: 'all 0.2s',
        backgroundColor: 'var(--primary)',
        color: 'var(--secondary)',
        boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
        opacity: '0.5', // 设置默认透明度
    });

    // 添加鼠标悬停效果
    trigger.onmouseover = () => {
        trigger.style.opacity = '1'; // 悬停时变为不透明
    };

    trigger.onmouseout = () => {
        trigger.style.opacity = '0.5'; // 鼠标移出后恢复透明度
    };

    // 拖动功能
    let isDragging = false;
    let offsetX = 0;
    let offsetY = 0;

    container.addEventListener('mousedown', (e) => {
        isDragging = true;
        const rect = container.getBoundingClientRect();
        offsetX = e.clientX - rect.left;
        offsetY = e.clientY - rect.top;
    });

    document.addEventListener('mousemove', (e) => {
        if (isDragging) {
            const x = e.clientX - offsetX;
            const y = e.clientY - offsetY;
            container.style.left = `${x}px`;
            container.style.top = `${y}px`;
            container.style.right = 'auto';
            container.style.bottom = 'auto';
        }
    });

    document.addEventListener('mouseup', () => {
        if (isDragging) {
            const rect = container.getBoundingClientRect();
            GM_setValue('b64Position', {
                x: rect.left,
                y: rect.top
            });
        }
        isDragging = false;
    });

    // 下拉菜单容器
    const menu = document.createElement('div');
    menu.style.cssText = `
        position: absolute;
        bottom: 100%;
        right: 0;
        width: 140px;
        background: var(--secondary);
        border-radius: 6px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        margin-bottom: 8px;
        opacity: 0;
        transform: translateY(10px);
        transition: all 0.25s ease;
        pointer-events: none;
        overflow: hidden;
    `;

    // 创建菜单项
    const createMenuItem = (text) => {
        const item = document.createElement('div');
        item.textContent = text;
        item.style.cssText = `
            padding: 10px 16px;
            font-size: 13px;
            cursor: pointer;
            color: var(--primary);
            transition: all 0.2s;
            text-align: left;
            line-height: 1.4;
        `;

        // 根据按钮文本设置不同圆角
        if (text === '解析本页 Base64') {
            item.style.borderRadius = '6px 6px 0 0'; // 上边圆角
        } else if (text === '文本转 Base64') {
            item.style.borderRadius = '0 0 6px 6px'; // 下边圆角
        }

        item.onmouseenter = () => {
            item.style.background = 'rgba(0,0,0,0.08)';
        };

        item.onmouseleave = () => {
            item.style.background = '';
        };

        return item;
    };

    const decodeItem = createMenuItem('解析本页 Base64');
    const encodeItem = createMenuItem('文本转 Base64');

    menu.append(decodeItem, encodeItem);
    container.append(trigger, menu);

    // 菜单切换逻辑
    const toggleMenu = (show) => {
        menu.style.opacity = show ? 1 : 0;
        menu.style.transform = show ? 'translateY(0)' : 'translateY(10px)';
        menu.style.pointerEvents = show ? 'all' : 'none';
    };

    // 主题适配
    const updateTheme = () => {
        const isDark = document.body.getAttribute('data-theme') === 'dark';
        trigger.style.backgroundColor = isDark ? 'var(--primary)' : 'var(--secondary)';
        trigger.style.color = isDark ? 'var(--secondary)' : 'var(--primary)';
        trigger.style.opacity = '0.8'; // 确保透明度在主题切换时更新
    };

    // 复制提示样式
    const notificationStyle = document.createElement('style');
    notificationStyle.textContent = `
        .b64-copy-notification {
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0, 200, 0, 0.9);
            color: white;
            padding: 10px 20px;
            border-radius: 4px;
            font-size: 14px;
            z-index: 10000;
            animation: fadeOut 2s forwards;
            animation-delay: 0.5s;
        }

        @keyframes fadeOut {
            from { opacity: 1; }
            to { opacity: 0; }
        }
    `;
    document.head.appendChild(notificationStyle);

    // 显示复制提示
    function showCopyNotification() {
        const notification = document.createElement('div');
        notification.className = 'b64-copy-notification';
        notification.textContent = '已复制到剪贴板';
        document.body.appendChild(notification);

        setTimeout(() => {
            notification.remove();
        }, 2500);
    }

    // 解码功能
    let isParsed = false;
    let originalTexts = {};

    const decodeBase64 = () => {
        const base64Regex = /([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?/g;

        if (isParsed) {
            Object.entries(originalTexts).forEach(([wrapperId, text]) => {
                const wrapper = document.getElementById(wrapperId);
                if (wrapper) {
                    const textNode = document.createTextNode(text);
                    wrapper.replaceWith(textNode);
                }
            });
            originalTexts = {};
            isParsed = false;
        } else {
            document.querySelectorAll('div.post, div.cooked').forEach(container => {
                const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);

                while (walker.nextNode()) {
                    const node = walker.currentNode;
                    if (node.textContent.length > 20) {
                        const decodedContent = node.textContent.replace(base64Regex, match => {
                            try {
                                const decoded = decodeURIComponent(escape(atob(match)));
                                return decoded !== match ?
                                    `<span class="b64-decoded" style="color: #FF7F28; background: rgba(255, 127, 40, 0.1); cursor: pointer; transition: all 0.2s; padding: 1px 2px; border-radius: 3px;">${decoded}</span>` :
                                    match;
                            } catch {
                                return match;
                            }
                        });

                        if (decodedContent !== node.textContent) {
                            const wrapper = document.createElement('span');
                            const wrapperId = `b64-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
                            wrapper.id = wrapperId;
                            wrapper.innerHTML = decodedContent;

                            wrapper.querySelectorAll('.b64-decoded').forEach(span => {
                                span.onclick = (e) => {
                                    GM_setClipboard(span.textContent);
                                    showCopyNotification();
                                    e.target.style.opacity = '0.7';
                                    setTimeout(() => e.target.style.opacity = '', 500);
                                };
                            });

                            originalTexts[wrapperId] = node.textContent;
                            node.replaceWith(wrapper);
                        }
                    }
                }
            });
            isParsed = true;
        }
    };

    // 编码功能
    const encodeBase64 = () => {
        const text = prompt('请输入要编码的文本:');
        if (text) {
            const encoded = btoa(unescape(encodeURIComponent(text)));
            GM_setClipboard(encoded);
            alert('已复制到剪贴板: ' + encoded);
        }
    };

    // 事件绑定
    trigger.addEventListener('click', (e) => {
        e.stopPropagation();
        toggleMenu(menu.style.opacity === '0');
    });

    decodeItem.addEventListener('click', () => {
        decodeBase64();
        toggleMenu(false);
    });

    encodeItem.addEventListener('click', () => {
        encodeBase64();
        toggleMenu(false);
    });

    // 初始化
    document.body.appendChild(container);
    updateTheme();

    // 主题变化监听
    new MutationObserver(updateTheme).observe(document.body, {
        attributes: true,
        attributeFilter: ['data-theme']
    });

    // 点击外部关闭
    document.addEventListener('click', (e) => {
        if (!container.contains(e.target)) toggleMenu(false);
    });

    // 全局样式
    const style = document.createElement('style');
    style.textContent = `
        .b64-decoded:hover {
            background: rgba(255, 127, 40, 0.2) !important;
        }
        .b64-decoded:active {
            background: rgba(255, 127, 40, 0.3) !important;
        }
    `;
    document.head.appendChild(style);
})();