Discord Image Save

Save actual Discord image and generate HTML link back to original message, then close image tab

// ==UserScript==
// @name         Discord Image Save
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Save actual Discord image and generate HTML link back to original message, then close image tab
// @match        https://discord.com/channels/*
// @match        https://media.discordapp.net/*
// @match        https://cdn.discordapp.com/*
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const currentUrl = window.location.href;

    // 获取用户设置(或使用默认值)
    let subFolder = GM_getValue('subFolder', '');

    // ---------- Global Config Toggle ----------
    GM_registerMenuCommand("✅ 启用自定义名称", async () => {
        await GM_setValue('useCustomName', true);
        alert('已启用自定义名称');
    });
    GM_registerMenuCommand("❌ 禁用自定义名称", async () => {
        await GM_setValue('useCustomName', false);
        alert('已禁用自定义名称');
    });
    GM_registerMenuCommand("✅ 启用保存HTML", async () => {
        await GM_setValue('enableHtml', true);
        alert('已启用保存 HTML 跳转页');
    });
    GM_registerMenuCommand("❌ 禁用保存HTML", async () => {
        await GM_setValue('enableHtml', false);
        alert('已禁用保存 HTML 跳转页');
    });

    GM_registerMenuCommand("📁 设置子文件夹", async () => {
        const current = await GM_getValue('subFolder', '');
        const newVal = prompt(`请输入子文件夹名称(留空则不使用子文件夹),当前的子文件夹为${current || '无'}`, current);
        if (newVal !== null) {
            await GM_setValue('subFolder', newVal.trim());
            alert(`子文件夹设置为:${newVal.trim() || '无'}`);
        }
    });

    // ---------- STEP 1: In Discord preview page ----------
    if (currentUrl.includes('discord.com/channels/')) {
        const observer = new MutationObserver(() => {
            const modal = document.querySelector('[class*="carouselModal"]');
            const existing = document.querySelector('#save-discord-jump');
            const openBtn = [...document.querySelectorAll('button')].find(b => b.getAttribute('aria-label') === '在浏览器中打开');
            if (modal && openBtn && !existing) {
                injectButton(openBtn);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });

        function injectButton(referenceBtn) {
            const btn = document.createElement('button');
            btn.innerText = '⬇';
            btn.id = 'save-discord-jump';
            btn.style = `
                margin-left: 10px;
                padding: 6px 10px;
                background-color: #5865F2;
                color: white;
                border: none;
                border-radius: 5px;
                cursor: pointer;
            `;

            btn.onclick = async () => {
                const useCustom = await GM_getValue('useCustomName', true);
                let customName = null;
                if (useCustom) {
                    const nameInput = prompt("请输入角色名称:", "");
                    if (!nameInput) return;
                    customName = nameInput.trim();
                }

                await GM_setValue('lastDiscordURL', window.location.href);
                await GM_setValue('customCharName', customName);

                // 在确认输入之后,立即跳转下载页面
                const openBtn = [...document.querySelectorAll('button')].find(b => b.getAttribute('aria-label') === '在浏览器中打开');
                if (openBtn) openBtn.click();
            };

            referenceBtn.parentElement?.appendChild(btn);
        }

        // ---------- STEP 1.5: 添加 JSON 文件下载增强按钮 ----------
        function enhanceJsonDownloadLinks() {
            const anchors = document.querySelectorAll('a[href*=".json"]:not([data-enhanced])');
            anchors.forEach(async anchor => {
                anchor.dataset.enhanced = "true";

                const href = anchor.href;
                const fileMatch = href.match(/\/([^\/?#]+\.json)/);
                if (!fileMatch) return;

                const filename = fileMatch[1];
                const subFolder = await GM_getValue('subFolder', '');
                const useCustom = await GM_getValue('useCustomName', true);

                let finalName = filename;
                if (useCustom) {
                    const customName = await GM_getValue('customCharName', 'file');
                    finalName = `${customName}.json`;
                }
                const fullName = subFolder ? `${subFolder}/${finalName}` : finalName;

                // 创建按钮
                const btn = document.createElement('button');
                btn.innerText = '⬇ JSON';
                btn.style = `
                    margin-left: 8px;
                    font-size: 12px;
                    padding: 2px 6px;
                    background-color: #5865F2;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                `;

                btn.onclick = () => {
                    GM_download(href, fullName);
                };

                anchor.parentElement?.appendChild(btn);
            });
        }

        // 启动 observer 动态检查 DOM 中是否有 JSON 链接加载出来
        const jsonObserver = new MutationObserver(() => {
            enhanceJsonDownloadLinks();
        });
        jsonObserver.observe(document.body, { childList: true, subtree: true });
    }

    // ---------- STEP 2: In image preview page ----------
    else if (currentUrl.includes('discordapp.com') || currentUrl.includes('discordapp.net')) {
        let downloaded = false;

        const tryDownload = async () => {
            if (downloaded) return;
            const trueImg = document.querySelector('img');
            if (!trueImg || !trueImg.src || !trueImg.src.startsWith('https://cdn.discordapp.com')) return;

            downloaded = true;

            const customName = await GM_getValue('customCharName', 'character');
            const subpath = await GM_getValue('subFolder', '');

            const imageUrl = trueImg.src.split('?')[0];
            const basename = customName ? `${customName}.png` : imageUrl.split('/').pop();
            const filename = subpath ? `${subpath}/${basename}` : basename;
            GM_download(trueImg.src, filename);

            const stem = basename.split('.')[0];
            const htmlname = `${stem}.html`
            const htmlfilename = subFolder ? `${subFolder}/${htmlname}` : htmlname;

            const enableHtml = await GM_getValue('enableHtml', true);
            if (enableHtml) {
                const jumpLink = await GM_getValue('lastDiscordURL', 'https://discord.com/channels/@me');
                const htmlContent = `<!DOCTYPE html><html><head><meta charset='utf-8'><meta http-equiv="refresh" content="0; url=${jumpLink}"></head><body><p><a href="${jumpLink}">前往 Discord 消息链接</a></p></body></html>`;
                const blob = new Blob([htmlContent], { type: 'text/html' });

                GM_download({
                    url: blob,
                    name: htmlfilename
                });
            }

            setTimeout(() => {
                window.close();
            }, 800);
        };

        const observer = new MutationObserver(() => {
            tryDownload();
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }
})();