Encode/Decode Base64 or Hex with Copy Option

Encode or decode Base64 or Hex strings and copy to clipboard

// ==UserScript==
// @name         Encode/Decode Base64 or Hex with Copy Option
// @version      1.8
// @description  Encode or decode Base64 or Hex strings and copy to clipboard
// @author       itisme
// @include      https://*
// @include      http://*
// @exclude      *://youtube.com/*
// @exclude      *://github.com/*
// @icon         https://data.voz.vn/styles/next/xenforo/smilies/popopo/sexy_girl.png?v=01
// @license      GPL-3.0
// @run_at       document_idle
// @grant        GM_addStyle
// @namespace http://tampermonkey.net/
// ==/UserScript==
(function() {
    'use strict';
    GM_addStyle(`
        :root {
            --overlay-bg: rgba(0, 0, 0, 0.7);
            --overlay-color: #ffffff;
            --font-color: #333333;
            --close-color: #f44336;
            --close-hover-color: #d32f2f;
            --button-bg: #007bff;
            --button-active-bg: #0056b3;
            --copy-bg: #28a745;
            --copy-active-bg: #195427;
            --copy-hover-bg: #218838;
            --clear-bg: #dc3545;
            --clear-active-bg: #c82333;
            --shadow: 0 6px 12px rgba(0, 0, 0, 0.25);
            --transition: 0.3s ease;
        }

        .my-overlay-container {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: var(--overlay-bg);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 99999;
            max-height: 100vh;
            overflow: auto;
        }

        .my-overlay {
            background: var(--overlay-color);
            padding: 20px;
            border-radius: 10px;
            width: 100%;
            max-width: 600px;
            position: relative;
            box-shadow: var(--shadow);
            transition: var(--transition);
        }

        .my-overlay-title {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            position: relative;
        }

        .my-overlay-title-text {
            font-size: 26px;
            font-weight: 600;
            color: var(--font-color);
        }

        .my-overlay-titleCloser {
            font-size: 30px;
            cursor: pointer;
            color: var(--close-color);
            transition: var(--transition);
        }

        .my-overlay-titleCloser:hover {
            color: var(--close-hover-color);
        }

        .my-overlay-content {
            display: flex;
            flex-direction: column;
        }

        .my-overlay-content label {
            margin-bottom: 8px;
            font-weight: 600;
            font-size: 16px;
        }

        .my-overlay-content select,
        .my-overlay-content textarea {
            width: 100%;
            margin-bottom: 20px;
            padding: 12px;
            border: 1px solid #ccc;
            border-radius: 5px;
            box-sizing: border-box;
        }

        .my-overlay-content textarea {
            height: 120px;
            font-size: 16px;
            line-height: 1.5;
            resize: vertical;
        }

        .my-button-container {
            display: flex;
            gap: 20px;
        }

        .my-button-container button {
            padding: 12px 20px;
            font-size: 14px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            flex: 1;
            transition: background-color var(--transition), box-shadow var(--transition);
            box-shadow: var(--shadow);
        }

        #actionButton {
            background-color: var(--button-bg);
            color: #ffffff;
        }

        #actionButton:hover,
        #actionButton:active {
            background-color: var(--button-active-bg);
        }

        #copyButton {
            background-color: var(--copy-bg);
            color: #ffffff;
        }

        #copyButton:hover {
            background-color: var(--copy-hover-bg);
        }

        #copyButton:active {
            background-color: var(--copy-active-bg);
        }

        #copyButton.flash {
            animation: flash 1s;
            background-color: var(--copy-bg);
        }

        @keyframes flash {
            0% {
                background-color: var(--copy-bg);
            }
            100% {
                background-color: var(--copy-active-bg);
            }
        }

        .custom-popup-link {
            position: fixed;
            width: 40px;
            height: 40px;
            background-image: url("https://data.voz.vn/styles/next/xenforo/smilies/popopo/sexy_girl.png?v=01");
            background-size: cover;
            background-position: center;
            border: none;
            cursor: pointer;
            z-index: 2147483647;
            user-select: none;
            background-color: transparent;
            transition: background-color var(--transition);
        }
    `);
    const createToggleButton = () => {
        const toggleButton = document.createElement('button');
        toggleButton.className = 'custom-popup-link';
        document.body.appendChild(toggleButton);
        // Load saved position
        const savedPosition = JSON.parse(localStorage.getItem('toggleButtonPosition')) || {
            top: 673,
            left: 1468
        };
        Object.assign(toggleButton.style, {
            top: `${savedPosition.top}px`,
            left: `${savedPosition.left}px`
        });
        makeDraggable(toggleButton);
    };
    const makeDraggable = (element) => {
        if (!element) return console.error('Phần tử không tồn tại.');
        let offsetX, offsetY, startX, startY, isDragging = false;
        element.style.userSelect = 'none';
        const setPosition = (x, y) => {
            element.style.left = `${x}px`;
            element.style.top = `${y}px`;
        };
        const moveElement = (e) => {
            if (!isDragging) {
                if (Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5) {
                    isDragging = true;
                }
                return;
            }
            const rect = element.getBoundingClientRect();
            const windowWidth = window.innerWidth;
            const windowHeight = window.innerHeight;
            let newLeft = e.clientX - offsetX;
            let newTop = e.clientY - offsetY;

            newLeft = Math.max(0, Math.min(newLeft, windowWidth - rect.width));
            newTop = Math.max(0, Math.min(newTop, windowHeight - rect.height));
            setPosition(newLeft, newTop);
        };
        const stopDragging = (e) => {
            if (isDragging) {
                localStorage.setItem('toggleButtonPosition', JSON.stringify({
                    top: element.style.top,
                    left: element.style.left
                }));
            } else if (e.button === 0 && typeof toggleOverlay === 'function') {
                toggleOverlay();
            }
            document.removeEventListener('mousemove', moveElement);
            document.removeEventListener('mouseup', stopDragging);
            document.removeEventListener('mouseleave', stopDragging);
            isDragging = false;
        };
        const startDragging = (e) => {
            if (e.button !== 0) return;
            const rect = element.getBoundingClientRect();
            offsetX = e.clientX - rect.left;
            offsetY = e.clientY - rect.top;
            startX = e.clientX;
            startY = e.clientY;
            document.addEventListener('mousemove', moveElement);
            document.addEventListener('mouseup', stopDragging);
            document.addEventListener('mouseleave', stopDragging);
        };
        element.addEventListener('mousedown', startDragging);
        const savedPosition = JSON.parse(localStorage.getItem('toggleButtonPosition'));
        if (savedPosition) {
            setPosition(
                parseFloat(savedPosition.left) || 0,
                parseFloat(savedPosition.top) || 0
            );
        }
    };
    window.addEventListener('load', () => {
        const toggleButton = document.querySelector('.toggleButtonClass');
        if (toggleButton) {
            makeDraggable(toggleButton);
        }
    });
    const toggleOverlay = () => {
        const overlayContainer = document.querySelector('.my-overlay-container');
        if (overlayContainer) {
            overlayContainer.style.display = (overlayContainer.style.display === 'none') ? 'flex' : 'none';
        } else if (typeof createOverlay === 'function') {
            createOverlay();
            const newOverlayContainer = document.querySelector('.my-overlay-container');
            if (newOverlayContainer) {
                newOverlayContainer.style.display = 'flex';
            } else {
                console.error('Không thể tìm thấy phần tử .my-overlay-container sau khi tạo.');
            }
        } else {
            console.error('Hàm createOverlay không tồn tại.');
        }
    };
    const createOverlay = () => {
        const overlayContainer = document.createElement('div');
        overlayContainer.className = 'my-overlay-container';
        overlayContainer.innerHTML = `
            <div class="my-overlay">
                <div class="my-overlay-title">
                    <span class="my-overlay-title-text">Encode/Decode Input</span>
                    <a class="my-overlay-titleCloser" role="button" aria-label="Close">×</a>
                </div>
                <div class="my-overlay-content">
                    <label>Select Operation:</label>
                    <select id="operationType">
                        <option value="decode">Decode</option>
                        <option value="encode">Encode</option>
                    </select>
                    <label>Select Type:</label>
                    <select id="decodeType">
                        <option value="hex">Hex</option>
                        <option value="base64">Base64</option>
                    </select>
                    <textarea id="inputString" placeholder="Enter text..."></textarea>
                    <textarea id="resultOutput" readonly placeholder="Result..."></textarea>
                    <div class="my-button-container">
                        <button id="actionButton">Decode/Encode</button>
                        <button id="copyButton">Copy</button>
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(overlayContainer);

        const closeButton = document.querySelector('.my-overlay-titleCloser');
        closeButton.addEventListener('click', () => {
            overlayContainer.style.display = 'none';
            document.getElementById('inputString').value = '';
            document.getElementById('resultOutput').value = '';
            document.getElementById('operationType').value = "decode";
            document.getElementById('decodeType').value = "hex";
        });

        const encodeHex = s => Array.from(s, c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ').toUpperCase();
        const decodeHex = h => {
            try {
                const cleanedHex = h.replace(/\s+/g, '');
                if (!/^[a-fA-F0-9]+$/.test(cleanedHex)) throw new Error('Invalid hex string');
                if (cleanedHex.length % 2 !== 0) throw new Error('Invalid hex string length');
                return cleanedHex.match(/../g).map(b => String.fromCharCode(parseInt(b, 16))).join('');
            } catch (err) {
                throw new Error(err.message);
            }
        };
        const encodeBase64 = b => btoa(b);
        const decodeBase64 = b => {
            try {
                if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/.test(b)) {
                    throw new Error('Invalid base64 string');
                }
                return atob(b);
            } catch (err) {
                throw new Error(err.message);
            }
        };

        document.getElementById('actionButton').addEventListener('click', () => {
            const op = document.getElementById('operationType').value;
            const type = document.getElementById('decodeType').value;
            const input = document.getElementById('inputString').value.trim();
            if (!input) return alert('Please enter a string.');
            try {
                let result;
                switch (type) {
                    case 'base64':
                        result = (op === 'decode') ? decodeBase64(input) : encodeBase64(input);
                        break;
                    case 'hex':
                        result = (op === 'decode') ? decodeHex(input) : encodeHex(input);
                        break;
                    default:
                        result = (op === 'decode') ? decodeURIComponent(input) : encodeURIComponent(input);
                }
                document.getElementById('resultOutput').value = result;
            } catch (err) {
                console.error(`Error [${type}]:`, err, 'Input:', input);
                alert(`Error: ${err.message}`);
            }
        });

        document.getElementById('copyButton').addEventListener('click', async () => {
            const resultOutput = document.getElementById('resultOutput');
            const textToCopy = resultOutput.value.trim();
            if (!textToCopy) {
                alert('Nothing to copy!');
                return;
            }
            try {
                await navigator.clipboard.writeText(textToCopy);
                alert('Copied to clipboard!');
                const copyButton = document.getElementById('copyButton');
                copyButton.classList.add('flash');
                setTimeout(() => copyButton.classList.remove('flash'), 1000);
            } catch (err) {
                console.error('Error copying to clipboard:', err);
                alert('Error copying to clipboard.');
            }
        });
    }
    createToggleButton();
})();