miaowuduck / Rongguang Image Decoder + Local Encryptor

// ==UserScript==
// @name         Rongguang Image Decoder + Local Encryptor
// @name:zh-CN   荣光解码助手 + 本地加密
// @namespace    https://yourname.example.com/
// @version      1.0
// @description  Decode images when title contains "荣光", plus floating button to encrypt local images
// @match        *://shuiyuan.sjtu.edu.cn/t/topic/*
// @author       Your Name
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // === Check if the special keyword "荣光" exists in the topic title ===
    const specialElem = document.querySelector("#ember3 > div.drop-down-mode.d-header-wrap > header > div > div > div.two-rows.extra-info-wrapper > div > div > h1 > a > span");
    if (!specialElem || !specialElem.textContent.includes("荣光")) {
        console.log("荣光 keyword not found, Rongguang Image Decoder Helper is disabled.");
        return;
    }

    const processed = new WeakSet();
    const cacheMap = new WeakMap();

    /** === Worker script === **/
    const workerScript = `
        self.onmessage = async function(e) {
            const { buffer, width, height, mode } = e.data;
            const blob = new Blob([buffer]);
            const imgBitmap = await createImageBitmap(blob);
            const canvas = new OffscreenCanvas(width || imgBitmap.width, height || imgBitmap.height);
            const ctx = canvas.getContext("2d");
            ctx.drawImage(imgBitmap, 0, 0);

            const imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height);
            const imgdata2 = new ImageData(canvas.width, canvas.height);

            function gilbert2d(width, height) {
                const coordinates = [];
                if (width >= height) generate2d(0, 0, width, 0, 0, height, coordinates);
                else generate2d(0, 0, 0, height, width, 0, coordinates);
                return coordinates;
            }
            function generate2d(x, y, ax, ay, bx, by, coordinates) {
                const w = Math.abs(ax + ay), h = Math.abs(bx + by);
                const dax = Math.sign(ax), day = Math.sign(ay);
                const dbx = Math.sign(bx), dby = Math.sign(by);
                if (h === 1) { for (let i = 0; i < w; i++) { coordinates.push([x, y]); x += dax; y += day; } return; }
                if (w === 1) { for (let i = 0; i < h; i++) { coordinates.push([x, y]); x += dbx; y += dby; } return; }
                let ax2 = Math.floor(ax / 2), ay2 = Math.floor(ay / 2);
                let bx2 = Math.floor(bx / 2), by2 = Math.floor(by / 2);
                const w2 = Math.abs(ax2 + ay2), h2 = Math.abs(bx2 + by2);
                if (2 * w > 3 * h) {
                    if ((w2 % 2) && (w > 2)) { ax2 += dax; ay2 += day; }
                    generate2d(x, y, ax2, ay2, bx, by, coordinates);
                    generate2d(x + ax2, y + ay2, ax - ax2, ay - ay2, bx, by, coordinates);
                } else {
                    if ((h2 % 2) && (h > 2)) { bx2 += dbx; by2 += dby; }
                    generate2d(x, y, bx2, by2, ax2, ay2, coordinates);
                    generate2d(x + bx2, y + by2, ax, ay, bx - bx2, by - by2, coordinates);
                    generate2d(x + (ax - dax) + (bx2 - dbx), y + (ay - day) + (by2 - dby),
                               -bx2, -by2, -(ax - ax2), -(ay - ay2), coordinates);
                }
            }

            const curve = gilbert2d(canvas.width, canvas.height);
            const offset = Math.round((Math.sqrt(5) - 1) / 2 * canvas.width * canvas.height);
            let i = 0, total = canvas.width * canvas.height;
            const CHUNK_SIZE = 5000;

            function processChunk() {
                const end = Math.min(i + CHUNK_SIZE, total);
                for (; i < end; i++) {
                    let srcIndex, dstIndex;
                    if (mode === "decode") {
                        const old_pos = curve[i];
                        const new_pos = curve[(i + offset) % total];
                        srcIndex = 4 * (new_pos[0] + new_pos[1] * canvas.width);
                        dstIndex = 4 * (old_pos[0] + old_pos[1] * canvas.width);
                    } else {
                        const new_pos = curve[i];
                        const old_pos = curve[(i + offset) % total];
                        srcIndex = 4 * (new_pos[0] + new_pos[1] * canvas.width);
                        dstIndex = 4 * (old_pos[0] + old_pos[1] * canvas.width);
                    }
                    imgdata2.data.set(imgdata.data.slice(srcIndex, srcIndex + 4), dstIndex);
                }
                self.postMessage({ progress: Math.floor((i / total) * 100) });
                if (i < total) setTimeout(processChunk, 0);
                else {
                    ctx.putImageData(imgdata2, 0, 0);
                    canvas.convertToBlob({ type: "image/jpeg", quality: 0.95 })
                        .then(blob => {
                            const reader = new FileReader();
                            reader.onloadend = () => {
                                self.postMessage({ done: true, dataUrl: reader.result });
                            };
                            reader.readAsDataURL(blob);
                        });
                }
            }
            processChunk();
        };
    `;

    function createWorker() {
        const blob = new Blob([workerScript], { type: "application/javascript" });
        return new Worker(URL.createObjectURL(blob));
    }

    function updateButtonProgress(btn, percent, text) {
        btn.textContent = text;
        btn.style.background = `linear-gradient(to right, rgba(40, 167, 69, 0.6) ${percent}%, rgba(255, 255, 255, 0.15) ${percent}%)`;
    }

    function downloadImageWithProgress(url, btn) {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.open('GET', url, true);
            xhr.responseType = 'arraybuffer';
            xhr.onprogress = (event) => {
                if (event.lengthComputable) {
                    const percent = Math.floor((event.loaded / event.total) * 50);
                    updateButtonProgress(btn, percent, `Downloading… ${percent}%`);
                }
            };
            xhr.onload = () => {
                if (xhr.status === 200) {
                    updateButtonProgress(btn, 50, `Download complete, decoding…`);
                    resolve(xhr.response);
                } else reject(new Error(`Download failed`));
            };
            xhr.onerror = () => reject(new Error("Network error"));
            xhr.send();
        });
    }

    async function toggleDecryption(originalUrl, img, btn) {
        const cache = cacheMap.get(img);
        if (!cache) {
            btn.disabled = true;
            try {
                const arrayBuffer = await downloadImageWithProgress(originalUrl, btn);
                const testBlob = new Blob([arrayBuffer]);
                const testImg = new Image();
                testImg.src = URL.createObjectURL(testBlob);
                await new Promise(r => testImg.onload = r);

                const worker = createWorker();
                worker.postMessage({ buffer: arrayBuffer, width: testImg.width, height: testImg.height, mode: "decode" }, [arrayBuffer]);
                worker.onmessage = (e) => {
                    if (e.data.progress !== undefined) {
                        updateButtonProgress(btn, e.data.progress, `Decoding… ${e.data.progress}%`);
                    }
                    if (e.data.done) {
                        img.removeAttribute("srcset");
                        img.removeAttribute("loading");
                        img.src = e.data.dataUrl;
                        cacheMap.set(img, { decodedUrl: e.data.dataUrl, originalUrl, state: "decoded" });
                        btn.disabled = false;
                        btn.textContent = "Show Original";
                        btn.style.background = "rgba(255, 255, 255, 0.15)";
                    }
                };
            } catch (err) {
                btn.textContent = "Load Failed";
                btn.style.background = "rgba(255,0,0,0.5)";
            }
        } else {
            if (cache.state === "decoded") {
                img.src = cache.originalUrl;
                cache.state = "original";
                btn.textContent = "Show Decoded";
                btn.style.background = "rgba(40, 167, 69, 0.4)";
            } else {
                img.src = cache.decodedUrl;
                cache.state = "decoded";
                btn.textContent = "Show Original";
                btn.style.background = "rgba(255, 255, 255, 0.15)";
            }
        }
    }

    function attachButton(img) {
        const aTag = img.closest('a.lightbox');
        if (!aTag || !aTag.dataset.downloadHref || processed.has(img)) return;
        processed.add(img);

        const originalUrl = new URL(aTag.dataset.downloadHref, window.location.origin).href;
        const wrapper = document.createElement("div");
        wrapper.style.position = "relative";
        wrapper.style.display = "inline-block";
        aTag.parentNode.insertBefore(wrapper, aTag);
        wrapper.appendChild(aTag);

        const btn = document.createElement("button");
        btn.innerText = "Decode";
        btn.style.position = "absolute";
        btn.style.left = "0px";
        btn.style.top = "0px";
        btn.style.zIndex = "1000";
        btn.style.minWidth = "140px";
        btn.style.height = "36px";
        btn.style.padding = "6px 16px";
        btn.style.borderRadius = "12px";
        btn.style.background = "rgba(255, 255, 255, 0.15)";
        btn.style.color = "#fff";
        btn.style.border = "1px solid rgba(255,255,255,0.3)";
        btn.style.backdropFilter = "blur(8px)";
        btn.style.webkitBackdropFilter = "blur(8px)";
        btn.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)";
        btn.style.fontWeight = "500";
        btn.style.fontSize = "14px";
        btn.style.cursor = "pointer";
        btn.style.whiteSpace = "nowrap";

        btn.onclick = () => toggleDecryption(originalUrl, img, btn);
        wrapper.appendChild(btn);
    }

    function processAllImages() {
        document.querySelectorAll('.cooked a.lightbox > img').forEach(attachButton);
    }

    window.addEventListener('load', processAllImages);
    const observer = new MutationObserver(m => {
        m.forEach(mu => mu.addedNodes.forEach(node => {
            if (node.nodeType === 1) {
                if (node.matches?.('.cooked a.lightbox > img')) attachButton(node);
                else node.querySelectorAll?.('.cooked a.lightbox > img').forEach(attachButton);
            }
        }));
    });
    observer.observe(document.body, { childList: true, subtree: true });

    /** === 新增:右上角圆形浮动按钮用于本地加密 === **/
    const floatBtn = document.createElement("button");
    floatBtn.textContent = "🔒";
    Object.assign(floatBtn.style, {
        position: "fixed",
        top: "20px",
        right: "20px",
        width: "50px",
        height: "50px",
        borderRadius: "50%",
        background: "rgba(255,255,255,0.15)",
        color: "#fff",
        border: "1px solid rgba(255,255,255,0.3)",
        backdropFilter: "blur(8px)",
        webkitBackdropFilter: "blur(8px)",
        boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
        fontSize: "20px",
        cursor: "pointer",
        zIndex: "9999"
    });
    document.body.appendChild(floatBtn);

    const fileInput = document.createElement("input");
    fileInput.type = "file";
    fileInput.accept = "image/*";
    fileInput.style.display = "none";
    document.body.appendChild(fileInput);

    floatBtn.addEventListener("click", () => fileInput.click());

    fileInput.addEventListener("change", () => {
        const file = fileInput.files[0];
        if (!file) return;
        const reader = new FileReader();
        reader.onload = function(evt) {
            const arrayBuffer = evt.target.result;
            const img = new Image();
            img.src = URL.createObjectURL(new Blob([arrayBuffer]));
            img.onload = () => {
                const worker = createWorker();
                worker.onmessage = (e) => {
                    if (e.data.done) {
                        const a = document.createElement("a");
                        a.href = e.data.dataUrl;
                        a.download = "encrypted.jpg";
                        a.click();
                    }
                };
                worker.postMessage({ buffer: arrayBuffer, width: img.width, height: img.height, mode: "encode" }, [arrayBuffer]);
            };
        };
        reader.readAsArrayBuffer(file);
    });

})();