网址(WebVPN)转化工具

在浙大 WebVPN 页面右下角提供网址转化工具。

// ==UserScript==
// @name         网址(WebVPN)转化工具
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  在浙大 WebVPN 页面右下角提供网址转化工具。
// @match        *://*.webvpn.zju.edu.cn/*
// @author       Slowist
// @grant        GM_addStyle
// @run-at       document-end
// @license      Apache 2.0
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        #converter-container-unique {
            --bg-color: #ffffff; --text-color: #212529; --io-bg-color: #ffffff;
            --io-text-color: #000000; --border-color: #dcdcdc; --shadow-color: rgba(0,0,0,0.15);
            --focus-ring-color: rgba(0, 123, 255, 0.25); --primary-accent-bg: #007bff;
            --primary-accent-hover-bg: #0056b3; --secondary-accent-bg: #28a745;
            --secondary-accent-hover-bg: #218838; --danger-color: #dc3545;
        }

        @media (prefers-color-scheme: dark) {
            #converter-container-unique {
                --bg-color: #2d2d2d; --text-color: #e0e0e0; --io-bg-color: #3a3a3a;
                --io-text-color: #f0f0f0; --border-color: #555555; --shadow-color: rgba(0,0,0,0.4);
                --focus-ring-color: rgba(0, 123, 255, 0.4);
            }
        }
        html[data-color-mode="dark"] #converter-container-unique,
        html[data-theme="dark"] #converter-container-unique {
            --bg-color: #2d2d2d; --text-color: #e0e0e0; --io-bg-color: #3a3a3a;
            --io-text-color: #f0f0f0; --border-color: #555555; --shadow-color: rgba(0,0,0,0.4);
            --focus-ring-color: rgba(0, 123, 255, 0.4);
        }

        #converter-container-unique {
            position: fixed; bottom: 25px; right: 25px; z-index: 99999;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            color-scheme: light dark;
        }
        #converter-container-unique button,
        #converter-container-unique textarea {
            font-family: inherit;
            font-size: 15px;
        }

        #converter-toggle-button-unique {
            width: 50px; height: 50px; border-radius: 50%; background-color: var(--primary-accent-bg);
            color: white; border: none; cursor: pointer; font-size: 24px; display: flex;
            align-items: center; justify-content: center; box-shadow: 0 4px 12px var(--shadow-color);
            transition: all 0.3s ease;
        }
        #converter-toggle-button-unique:hover { background-color: var(--primary-accent-hover-bg); transform: translateY(-2px); }

        #converter-window-unique {
            position: absolute; bottom: 65px; right: 0; width: 320px;
            background-color: var(--bg-color); color: var(--text-color); border-radius: 12px;
            box-shadow: 0 6px 25px var(--shadow-color); display: none; flex-direction: column;
            padding: 15px 20px; transform-origin: bottom right;
        }

        @keyframes scale-in-fade { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
        @keyframes scale-out-fade { from { transform: scale(1); opacity: 1; } to { transform: scale(0.8); opacity: 0; } }

        #converter-window-unique.visible {
            display: flex;
            animation: scale-in-fade 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
        }
        #converter-window-unique.hiding {
            animation: scale-out-fade 0.15s cubic-bezier(0.55, 0.085, 0.68, 0.53) forwards;
        }

        #converter-header-unique {
            display: flex; justify-content: space-between; align-items: center;
            margin-bottom: 12px;
        }
        #converter-title-unique { font-size: 16px; font-weight: 600; }
        .converter-header-controls-unique { display: flex; align-items: center; }
        .converter-header-button-unique {
            background: none; border: none; cursor: pointer;
            color: var(--text-color); opacity: 0.6;
            width: 28px; height: 28px;
            display:flex; align-items: center; justify-content: center;
            padding: 0;
        }
        .converter-header-button-unique:hover { opacity: 1; }
        #converter-disable-button-unique:hover { color: var(--danger-color); }

        .converter-main-content-unique {
            display: flex; flex-direction: column; gap: 12px;
        }

        .converter-output-wrapper-unique { position: relative; }
        #converter-copy-button-unique {
            position: absolute; top: 6px; right: 6px; width: 28px; height: 28px;
            display: flex; align-items: center; justify-content: center;
            background-color: var(--io-bg-color); border: 1px solid var(--border-color);
            color: var(--text-color); border-radius: 6px; cursor: pointer;
            transition: all 0.2s ease; opacity: 0.7;
        }
        #converter-copy-button-unique:hover { opacity: 1; border-color: var(--primary-accent-bg); }
        #converter-copy-button-unique.copied { color: var(--secondary-accent-bg); border-color: var(--secondary-accent-bg); }

        .converter-io-unique {
            width: 100%; padding: 10px; border: 1px solid var(--border-color); background-color: var(--io-bg-color);
            color: var(--io-text-color); border-radius: 6px; box-sizing: border-box;
            resize: vertical; transition: border-color 0.3s, box-shadow 0.3s;
        }
        #converter-output-unique { padding-right: 40px; }
        .converter-io-unique::placeholder { color: #888; }
        .converter-io-unique:focus { outline: none; border-color: var(--primary-accent-bg); box-shadow: 0 0 0 2px var(--focus-ring-color); }

        #converter-execute-button-unique {
            padding: 10px 15px; background-color: var(--secondary-accent-bg); color: white; border: none;
            border-radius: 6px; cursor: pointer; font-weight: 500;
            transition: background-color 0.3s; width: 100%; box-sizing: border-box;
        }
        #converter-execute-button-unique:hover { background-color: var(--secondary-accent-hover-bg); }

        #converter-footer-unique {
            text-align: center;
            padding-top: 4px;
        }
        #converter-footer-unique a {
            font-size: 13px;
            color: var(--primary-accent-bg);
            text-decoration: none;
        }
        #converter-footer-unique a:hover {
            text-decoration: underline;
        }
    `);

    const SVG_ICON_COPY = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
    const SVG_ICON_CHECK = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
    const SVG_ICON_MINIMIZE = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>`;
    const SVG_ICON_CLOSE = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`;

    const container = document.createElement('div');
    container.id = 'converter-container-unique';
    document.body.appendChild(container);

    const toggleButton = document.createElement('button');
    toggleButton.id = 'converter-toggle-button-unique';
    toggleButton.innerHTML = '⇄';
    container.appendChild(toggleButton);

    const windowDiv = document.createElement('div');
    windowDiv.id = 'converter-window-unique';
    windowDiv.innerHTML = `
        <div id="converter-header-unique">
            <span id="converter-title-unique">URL Converter</span>
            <div class="converter-header-controls-unique">
                <button id="converter-minimize-button-unique" class="converter-header-button-unique" title="Hide Window">${SVG_ICON_MINIMIZE}</button>
                <button id="converter-disable-button-unique" class="converter-header-button-unique" title="Hide until next refresh">${SVG_ICON_CLOSE}</button>
            </div>
        </div>
        <div class="converter-main-content-unique">
            <textarea id="converter-input-unique" class="converter-io-unique" rows="4" placeholder="Input Website Here..."></textarea>
            <button id="converter-execute-button-unique">Bake!</button>
            <div class="converter-output-wrapper-unique">
                <textarea id="converter-output-unique" class="converter-io-unique" rows="4" placeholder="Result..." readonly></textarea>
                <button id="converter-copy-button-unique" title="Copy to clipboard">${SVG_ICON_COPY}</button>
            </div>
            <div id="converter-footer-unique">
                <a href="https://webvpn.zju.edu.cn/" target="_blank">WebVPN 入口</a>
            </div>
        </div>
    `;
    container.appendChild(windowDiv);

    const executeButton = document.getElementById('converter-execute-button-unique');
    const inputArea = document.getElementById('converter-input-unique');
    const outputArea = document.getElementById('converter-output-unique');
    const copyButton = document.getElementById('converter-copy-button-unique');
    const disableButton = document.getElementById('converter-disable-button-unique');
    const minimizeButton = document.getElementById('converter-minimize-button-unique');

    function hideWindowWithAnimation() {
        if (!windowDiv.classList.contains('visible')) return;
        windowDiv.classList.add('hiding');
        setTimeout(() => {
            windowDiv.classList.remove('visible');
            windowDiv.classList.remove('hiding');
        }, 150);
    }

    toggleButton.addEventListener('click', (event) => {
        event.stopPropagation();
        if (windowDiv.classList.contains('visible')) {
            hideWindowWithAnimation();
        } else {
            windowDiv.classList.add('visible');
        }
    });

    windowDiv.addEventListener('click', (event) => event.stopPropagation());
    document.addEventListener('click', () => {
        hideWindowWithAnimation();
    });

    executeButton.addEventListener('click', () => {
        const inputText = inputArea.value;
        const regex = /^https?:\/\/([a-zA-Z0-9-]+)\.webvpn\.zju\.edu\.cn(?::\d+)?(\/.*)?$/;
        const matchResult = inputText.match(regex);
        let outputText;
        if (matchResult && matchResult[1]) {
            const capturedPart = matchResult[1]; const chunks = capturedPart.split('-');
            const lastChunk = chunks[chunks.length - 1];
            if (lastChunk === 's') {
                outputText = "https://" + chunks.slice(0, -1).join(".");
            } else {
                outputText = "http://" + chunks.join(".");
            }
            outputText += (matchResult[2] || "");
        } else {
            outputText = inputText;
        }
        outputArea.value = outputText;
    });

    copyButton.addEventListener('click', () => {
        if (!outputArea.value) return;
        navigator.clipboard.writeText(outputArea.value).then(() => {
            copyButton.innerHTML = SVG_ICON_CHECK;
            copyButton.classList.add('copied');
            setTimeout(() => {
                copyButton.innerHTML = SVG_ICON_COPY;
                copyButton.classList.remove('copied');
            }, 2000);
        }).catch(err => console.error('Failed to copy text: ', err));
    });

    minimizeButton.addEventListener('click', (event) => {
        event.stopPropagation();
        hideWindowWithAnimation();
    });

    disableButton.addEventListener('click', () => {
        container.remove();
    });

})();