Discourse Base64 Helper

Base64编解码工具 for Discourse论坛

目前為 2025-04-01 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Discourse Base64 Helper
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Base64编解码工具 for Discourse论坛
// @author       Xavier
// @match        *://linux.do/*
// @match        *://clochat.com/*
// @grant        GM_notification
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // 样式注入
    GM_addStyle(`
    .decoded-text {
        cursor: pointer;
        transition: all 0.2s;
        padding: 1px 3px;
        border-radius: 3px;
        background-color: #fff3cd !important;
        color: #664d03 !important;
    }

    .decoded-text:hover {
        background-color: #ffe69c !important;
    }

    @media (prefers-color-scheme: dark) {
        .decoded-text {
            background-color: #332100 !important;
            color: #ffd54f !important;
        }
        .decoded-text:hover {
            background-color: #664d03 !important;
        }
    }

    .menu-item[data-mode="restore"] {
        background: rgba(0, 123, 255, 0.1) !important;
    }
    `);

    // 初始化检测
    if (document.getElementById('base64-helper-root')) return;
    const container = document.createElement('div');
    container.id = 'base64-helper-root';
    document.body.append(container);
    const shadowRoot = container.attachShadow({ mode: 'open' });

    // Shadow DOM样式
    const style = document.createElement('style');
    style.textContent = `
    :host {
        all: initial !important;
        position: fixed !important;
        z-index: 2147483647 !important;
        pointer-events: none !important;
    }

    .base64-helper {
        position: fixed;
        z-index: 2147483647;
        cursor: move;
        font-family: system-ui, -apple-system, sans-serif;
        opacity: 0.5;
        transition: opacity 0.3s ease, transform 0.2s;
        pointer-events: auto !important;
        will-change: transform;
    }

    .base64-helper:hover {
        opacity: 1 !important;
    }

    .main-btn {
        background: #ffffff;
        color: #000000 !important;
        padding: 8px 16px;
        border-radius: 6px;
        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
        font-weight: 500;
        user-select: none;
        transition: all 0.2s;
        font-size: 14px;
        cursor: pointer;
        border: none !important;
        pointer-events: auto !important;
    }

    .menu {
        position: absolute;
        bottom: calc(100% + 5px);
        right: 0;
        background: #ffffff;
        border-radius: 6px;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        display: none;
        min-width: auto !important;
        width: max-content !important;
        pointer-events: auto !important;
        overflow: hidden;
    }

    .menu-item {
        padding: 8px 12px !important;
        color: #333 !important;
        transition: all 0.2s;
        font-size: 13px;
        cursor: pointer;
        position: relative;
        border-radius: 0 !important;
        isolation: isolate;
        white-space: nowrap !important;
    }

    .menu-item:hover::before {
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: currentColor;
        opacity: 0.1;
        z-index: -1;
    }

    .menu-item:first-child:hover::before {
        border-radius: 6px 6px 0 0;
    }

    .menu-item:last-child:hover::before {
        border-radius: 0 0 6px 6px;
    }

    @media (prefers-color-scheme: dark) {
        .main-btn {
            background: #2d2d2d;
            color: #fff !important;
            box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
        }
        .menu {
            background: #1a1a1a;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
        }
        .menu-item {
            color: #e0e0e0 !important;
        }
        .menu-item:hover::before {
            opacity: 0.08;
        }
    }

    @keyframes slideIn {
        from { top: -50px; opacity: 0; }
        to { top: 20px; opacity: 1; }
    }

    @keyframes fadeOut {
        from { opacity: 1; }
        to { opacity: 0; }
    }
    `;
    shadowRoot.appendChild(style);

    // 界面元素
    const uiContainer = document.createElement('div');
    uiContainer.className = 'base64-helper';

    const mainBtn = document.createElement('button');
    mainBtn.className = 'main-btn';
    mainBtn.textContent = 'Base64';

    const menu = document.createElement('div');
    menu.className = 'menu';

    const decodeBtn = document.createElement('div');
    decodeBtn.className = 'menu-item';
    decodeBtn.textContent = '解析本页Base64';
    decodeBtn.dataset.mode = 'decode';

    const encodeBtn = document.createElement('div');
    encodeBtn.className = 'menu-item';
    encodeBtn.textContent = '文本转Base64';

    menu.append(decodeBtn, encodeBtn);
    uiContainer.append(mainBtn, menu);
    shadowRoot.appendChild(uiContainer);

    // 核心功能
    let menuVisible = false;
    let isDragging = false;
    let startX, startY, initialX, initialY;
    const originalContents = new Map();

    // 位置管理
    const positionManager = {
        get: () => {
            const saved = GM_getValue('btnPosition');
            if (!saved) return null;

            const maxX = window.innerWidth - uiContainer.offsetWidth - 20;
            const maxY = window.innerHeight - uiContainer.offsetHeight - 20;

            return {
                x: Math.min(Math.max(saved.x, 20), maxX),
                y: Math.min(Math.max(saved.y, 20), maxY)
            };
        },
        set: (x, y) => {
            const pos = {
                x: Math.max(20, Math.min(x, window.innerWidth - uiContainer.offsetWidth - 20)),
                y: Math.max(20, Math.min(y, window.innerHeight - uiContainer.offsetHeight - 20))
            };

            GM_setValue('btnPosition', pos);
            return pos;
        }
    };

    // 初始化位置
    const initPosition = () => {
        const pos = positionManager.get() || {
            x: window.innerWidth - 120,
            y: window.innerHeight - 80
        };

        uiContainer.style.left = `${pos.x}px`;
        uiContainer.style.top = `${pos.y}px`;
    };
    initPosition();

    // 状态重置
    function resetState() {
        if (decodeBtn.dataset.mode === 'restore') {
            restoreOriginalContent();
            decodeBtn.textContent = '解析本页Base64';
            decodeBtn.dataset.mode = 'decode';
            originalContents.clear();
        }
    }

    // 事件监听
    mainBtn.addEventListener('click', function(e) {
        e.stopPropagation();
        menuVisible = !menuVisible;
        menu.style.display = menuVisible ? 'block' : 'none';
    });

    document.addEventListener('click', function(e) {
        if (menuVisible && !shadowRoot.contains(e.target)) {
            menuVisible = false;
            menu.style.display = 'none';
        }
    });

    // 拖拽功能
    mainBtn.addEventListener('mousedown', startDrag);
    document.addEventListener('mousemove', drag);
    document.addEventListener('mouseup', stopDrag);

    function startDrag(e) {
        isDragging = true;
        startX = e.clientX;
        startY = e.clientY;
        const rect = uiContainer.getBoundingClientRect();
        initialX = rect.left;
        initialY = rect.top;
        uiContainer.style.transition = 'none';
    }

    function drag(e) {
        if (!isDragging) return;
        const dx = e.clientX - startX;
        const dy = e.clientY - startY;

        const newX = initialX + dx;
        const newY = initialY + dy;

        const pos = positionManager.set(newX, newY);
        uiContainer.style.left = `${pos.x}px`;
        uiContainer.style.top = `${pos.y}px`;
    }

    function stopDrag() {
        isDragging = false;
        uiContainer.style.transition = 'opacity 0.3s ease';
    }

    // 窗口resize处理
    let resizeTimer;
    window.addEventListener('resize', () => {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(() => {
            const pos = positionManager.get();
            if (pos) {
                uiContainer.style.left = `${pos.x}px`;
                uiContainer.style.top = `${pos.y}px`;
            }
        }, 100);
    });

    // 页面导航事件
    window.addEventListener('popstate', resetState);
    window.addEventListener('turbo:render', resetState);
    window.addEventListener('discourse:before-auto-refresh', () => {
        GM_setValue('btnPosition', positionManager.get());
        resetState();
    });

    // 解析功能
    decodeBtn.addEventListener('click', function() {
        if (this.dataset.mode === 'restore') {
            restoreOriginalContent();
            this.textContent = '解析本页Base64';
            this.dataset.mode = 'decode';
            showNotification('已恢复原始内容', 'success');
            menu.style.display = 'none';
            return;
        }

        originalContents.clear();
        let hasValidBase64 = false;

        try {
            document.querySelectorAll('.cooked, .post-body').forEach(element => {
                const regex = /(?<!\w)([A-Za-z0-9+/]{6,}?={0,2})(?!\w)/g;
                let newHtml = element.innerHTML;
                let modified = false;

                Array.from(newHtml.matchAll(regex)).reverse().forEach(match => {
                    const original = match[0];
                    if (!validateBase64(original)) return;

                    try {
                        const decoded = decodeBase64(original);
                        originalContents.set(element, element.innerHTML);

                        newHtml = newHtml.substring(0, match.index) +
                            `<span class="decoded-text">${decoded}</span>` +
                            newHtml.substring(match.index + original.length);

                        hasValidBase64 = modified = true;
                    } catch(e) {}
                });

                if (modified) element.innerHTML = newHtml;
            });

            if (!hasValidBase64) {
                showNotification('本页未发现有效Base64内容', 'info');
                originalContents.clear();
                return;
            }

            document.querySelectorAll('.decoded-text').forEach(el => {
                el.addEventListener('click', copyToClipboard);
            });

            decodeBtn.textContent = '恢复本页Base64';
            decodeBtn.dataset.mode = 'restore';
            showNotification('解析完成', 'success');
        } catch (e) {
            showNotification('解析失败: ' + e.message, 'error');
            originalContents.clear();
        }

        menuVisible = false;
        menu.style.display = 'none';
    });

    // 编码功能
    encodeBtn.addEventListener('click', function() {
        const text = prompt('请输入要编码的文本:');
        if (text === null) return;

        try {
            const encoded = encodeBase64(text);
            GM_setClipboard(encoded);
            showNotification('Base64已复制', 'success');
        } catch (e) {
            showNotification('编码失败: ' + e.message, 'error');
        }
        menu.style.display = 'none';
    });

    // 改进的校验函数
    function validateBase64(str) {
        // 基础校验
        const validLength = str.length % 4 === 0;
        const validChars = /^[A-Za-z0-9+/]+={0,2}$/.test(str);
        const validPadding = !(str.includes('=') && !/==?$/.test(str));
        if (!validLength || !validChars || !validPadding) return false;

        // 移除填充后的校验
        const baseStr = str.replace(/=+$/, '');
        if (baseStr.length < 6) return false;
        const hasSpecialChar = /[+/0-9]/.test(baseStr);
        return hasSpecialChar;
    }

    // 工具函数
    function decodeBase64(str) {
        return decodeURIComponent(escape(atob(str)));
    }

    function encodeBase64(str) {
        return btoa(unescape(encodeURIComponent(str)));
    }

    function restoreOriginalContent() {
        originalContents.forEach((html, element) => {
            element.innerHTML = html;
        });
        originalContents.clear();
    }

    function copyToClipboard(e) {
        GM_setClipboard(e.target.innerText);
        showNotification('内容已复制', 'success');
        e.stopPropagation();
    }

    // 通知系统
    function showNotification(text, type) {
        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            padding: 12px 24px;
            border-radius: 6px;
            background: ${type === 'success' ? '#4CAF50' :
                        type === 'error' ? '#f44336' : '#2196F3'};
            color: white;
            z-index: 2147483647;
            animation: slideIn 0.3s forwards, fadeOut 0.3s 2s forwards;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            font-family: system-ui, -apple-system, sans-serif;
            pointer-events: none;
        `;
        notification.textContent = text;
        document.body.appendChild(notification);
        setTimeout(() => notification.remove(), 2300);
    }

    // 防冲突处理
    if (window.hasBase64Helper) return;
    window.hasBase64Helper = true;
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址