IPFS CID Copy Helper

为IPFS链接添加CID复制功能,支持多种IPFS/IPNS格式和批量复制

当前为 2024-10-29 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         IPFS CID Copy Helper
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  为IPFS链接添加CID复制功能,支持多种IPFS/IPNS格式和批量复制
// @author       cenglin123
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // 添加样式
    GM_addStyle(`
        .ipfs-copy-btn {
            display: none;
            position: absolute;
            background: #4a90e2;
            color: white;
            padding: 5px 10px;
            border-radius: 4px;
            font-size: 12px;
            cursor: pointer;
            z-index: 10000;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            transform: translateX(-50%);
        }
        .ipfs-copy-btn:hover {
            background: #357abd;
        }
        .ipfs-batch-buttons {
            position: fixed;
            bottom: 20px;
            right: 20px;
            display: flex;
            flex-direction: column;
            gap: 10px;
            z-index: 10000;
            transition: transform 0.3s ease;
            min-height: 100px; /* 设置固定的初始高度 */
        }
        .ipfs-batch-buttons.collapsed {
            transform: translateX(calc(100% + 20px));
        }
        .ipfs-batch-btn {
            background: #4a90e2;
            color: white;
            padding: 8px 15px;
            border-radius: 4px;
            font-size: 14px;
            cursor: pointer;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            display: none;
            position: relative;
            white-space: nowrap;
            transition: transform 0.3s ease;
        }
        .ipfs-batch-btn:hover {
            background: #357abd;
        }
        .ipfs-copy-count {
            background: #ff4444;
            color: white;
            border-radius: 50%;
            padding: 2px 6px;
            font-size: 12px;
            position: absolute;
            top: -8px;
            right: -8px;
        }
        .ipfs-toggle-btn {
            position: absolute;
            left: -28px;
            top: 0;
            width: 28px;
            height: 28px;
            background: #4a90e2;
            color: white;
            border: none;
            border-radius: 4px 0 0 4px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: -2px 0 5px rgba(0,0,0,0.2);
        }
        .ipfs-toggle-btn:hover {
            background: #357abd;
        }
        .ipfs-toggle-btn svg {
            width: 16px;
            height: 16px;
            transition: transform 0.3s ease;
            transform: rotate(180deg);
        }
        .collapsed .ipfs-toggle-btn svg {
            transform: rotate(0deg);
        }
    `);

    // 创建UI元素
    const copyBtn = document.createElement('div');
    copyBtn.className = 'ipfs-copy-btn';
    copyBtn.textContent = '复制 CID';
    document.body.appendChild(copyBtn);

    // 创建批量按钮容器
    const batchButtonsContainer = document.createElement('div');
    batchButtonsContainer.className = 'ipfs-batch-buttons';
    document.body.appendChild(batchButtonsContainer);

    // 创建收起/展开按钮
    const toggleBtn = document.createElement('button');
    toggleBtn.className = 'ipfs-toggle-btn';
    toggleBtn.innerHTML = `
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <polyline points="15 18 9 12 15 6"></polyline>
        </svg>
    `;
    batchButtonsContainer.appendChild(toggleBtn);

    // 创建批量复制CID按钮
    const batchCopyBtn = document.createElement('div');
    batchCopyBtn.className = 'ipfs-batch-btn';
    batchCopyBtn.innerHTML = '批量复制 CID <span class="ipfs-copy-count">0</span>';
    batchButtonsContainer.appendChild(batchCopyBtn);

    // 创建批量复制文件名按钮
    const batchFilenameBtn = document.createElement('div');
    batchFilenameBtn.className = 'ipfs-batch-btn';
    batchFilenameBtn.innerHTML = '批量复制文件名 <span class="ipfs-copy-count">0</span>';
    batchButtonsContainer.appendChild(batchFilenameBtn);

    // 提取文件名的函数
    function extractFilename(url, linkText) {
        // 从 URL 参数中获取文件名
        const filenameParam = new URL(url).searchParams.get('filename');
        if (filenameParam) {
            return decodeURIComponent(filenameParam);
        }

        // 从路径中获取文件名
        const pathParts = new URL(url).pathname.split('/');
        const lastPart = pathParts[pathParts.length - 1];

        // 如果路径最后一部分不是 CID,则可能是文件名
        if (lastPart && !lastPart.match(/^(Qm[a-zA-Z0-9]{44}|baf[a-zA-Z0-9]+|k51[a-zA-Z0-9]+)$/i)) {
            return decodeURIComponent(lastPart);
        }

        // 使用链接文本作为备选
        if (linkText && linkText.trim() && !linkText.includes('...')) {
            return linkText.trim();
        }

        return null;
    }

    // 创建批量复制下载链接按钮
    const batchDownloadBtn = document.createElement('div');
    batchDownloadBtn.className = 'ipfs-batch-btn';
    batchDownloadBtn.innerHTML = '批量复制下载链接 <span class="ipfs-copy-count">0</span>';
    batchButtonsContainer.appendChild(batchDownloadBtn);

    // 存储页面上找到的所有链接信息
    const linkInfo = new Map(); // 存储CID和原始链接

    // 存储收起状态
    let isCollapsed = false;

    // 切换收起/展开状态
    function toggleCollapse() {
        isCollapsed = !isCollapsed;
        batchButtonsContainer.classList.toggle('collapsed', isCollapsed);
        // 保存状态到localStorage
        localStorage.setItem('ipfsCopyHelperCollapsed', isCollapsed);
    }

    // 初始化收起状态
    const savedCollapsedState = localStorage.getItem('ipfsCopyHelperCollapsed');
    if (savedCollapsedState !== null) {
        isCollapsed = (savedCollapsedState === 'true');
        batchButtonsContainer.classList.toggle('collapsed', isCollapsed);
    }

    // 配置选项 - 默认收起或展开
    GM_registerMenuCommand('切换右下角浮窗默认展开/收起状态', () => {
        const defaultCollapsed = localStorage.getItem('ipfsCopyHelperDefaultCollapsed');
        const newDefault = defaultCollapsed === 'true' ? 'false' : 'true';
        localStorage.setItem('ipfsCopyHelperDefaultCollapsed', newDefault);
        alert(`默认状态已更改为:${newDefault === 'true' ? '收起' : '展开'}`);
    });

    // 检查默认配置
    const defaultCollapsedState = localStorage.getItem('ipfsCopyHelperDefaultCollapsed');
    if (defaultCollapsedState === 'false') {
        isCollapsed = false;
        batchButtonsContainer.classList.remove('collapsed');
    } else if (defaultCollapsedState === 'true') {
        isCollapsed = true;
        batchButtonsContainer.classList.add('collapsed');
    }

    // 提取CID的函数
    function extractCID(url) {
        // 匹配子域名形式的CID (bafy... or Qm...)
        const subdomainMatch = url.match(/^https?:\/\/(baf[a-zA-Z0-9]{1,}|Qm[a-zA-Z0-9]{44})\./i);
        if (subdomainMatch) {
            return subdomainMatch[1];
        }

        // 匹配路径中的IPFS CID(包括 Qm 和 bafy 开头的格式)
        const ipfsPathMatch = url.match(/\/ipfs\/(Qm[a-zA-Z0-9]{44}|baf[a-zA-Z0-9]+)/i);
        if (ipfsPathMatch) {
            return ipfsPathMatch[1];
        }

        // 匹配 IPNS 的密钥
        const ipnsKeyMatch = url.match(/\/ipns\/(k51[a-zA-Z0-9]+)/i);
        if (ipnsKeyMatch) {
            return ipnsKeyMatch[1];
        }

        return null;
    }


    // 检测链接类型
    function detectLinkType(url) {
        if (url.includes('/ipns/') || url.includes('k51')) {
            return 'IPNS Key';
        }
        return 'IPFS CID';
    }

    // 扫描页面上的所有链接
    function scanPageForLinks() {
        const links = document.getElementsByTagName('a');
        const currentPageCID = extractCID(window.location.href);

        linkInfo.clear();
        for (const link of links) {
            const cid = extractCID(link.href);
            if (cid && cid !== currentPageCID) {
                const filename = extractFilename(link.href, link.textContent);
                linkInfo.set(cid, {
                    type: detectLinkType(link.href),
                    url: link.href,
                    text: link.textContent.trim(),
                    filename: filename
                });
            }
        }

        // 更新按钮显示和计数
        const count = linkInfo.size;
        const countElements = document.querySelectorAll('.ipfs-copy-count');
        countElements.forEach(el => {
            el.textContent = count;
        });

        if (count > 0) {
            batchCopyBtn.style.display = 'block';
            batchDownloadBtn.style.display = 'block';
            batchFilenameBtn.style.display = 'block';
        } else {
            batchCopyBtn.style.display = 'none';
            batchDownloadBtn.style.display = 'none';
            batchFilenameBtn.style.display = 'none';
        }
    }

    // 复制文本到剪贴板
    function copyToClipboard(text, button) {
        const originalText = button.textContent;
        navigator.clipboard.writeText(text).then(() => {
            button.textContent = '已复制!';
            setTimeout(() => {
                button.textContent = originalText;
            }, 1000);
        }).catch(err => {
            console.error('复制失败:', err);
            button.textContent = '复制失败';
            setTimeout(() => {
                button.textContent = originalText;
            }, 1000);
        });
    }

    // 批量复制CID
    function batchCopyCIDs() {
        const cids = Array.from(linkInfo.keys());
        console.log("批量复制的 CID:", cids); // 检查批量复制内容
        if (cids.length > 0) { // 仅在有内容时复制
            const formattedCIDs = cids.join('\n');
            copyToClipboard(formattedCIDs, batchCopyBtn);
        } else {
            console.log("没有可复制的 CID。");
        }
    }

    // 批量复制文件名
    function batchCopyFilenames() {
        const filenames = Array.from(linkInfo.values())
            .map(info => info.filename || '未知文件名')
            .filter(filename => filename !== '未知文件名'); // 过滤掉没有文件名的项

        if (filenames.length > 0) {
            const formattedFilenames = filenames.join('\n');
            copyToClipboard(formattedFilenames, batchFilenameBtn);
        } else {
            batchFilenameBtn.textContent = '没有可用的文件名';
            setTimeout(() => {
                batchFilenameBtn.innerHTML = '批量复制文件名 <span class="ipfs-copy-count">' +
                    linkInfo.size + '</span>';
            }, 1000);
        }
    }

    // 修改批量复制下载链接函数,优化文件名处理
    function batchCopyDownloadLinks() {
        const links = Array.from(linkInfo.values()).map(info => {
            let url = info.url;
            // 如果存在文件名且URL中没有filename参数,则添加
            if (info.filename && !url.includes('?filename=')) {
                url += (url.includes('?') ? '&' : '?') + 'filename=' +
                    encodeURIComponent(info.filename);
            }
            return url;
        });

        if (links.length > 0) {
            const formattedLinks = links.join('\n');
            copyToClipboard(formattedLinks, batchDownloadBtn);
        }
    }


    // 初始化页面扫描
    let scanTimeout;
    function initPageScan() {
        if (scanTimeout) {
            clearTimeout(scanTimeout);
        }
        scanTimeout = setTimeout(scanPageForLinks, 1000);
    }

    // 监听页面变化
    const observer = new MutationObserver((mutations) => {
        initPageScan();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // 初始扫描
    initPageScan();

    // 监听所有链接的鼠标事件
    document.addEventListener('mouseover', function(e) {
        const link = e.target.closest('a');
        if (!link) return;

        const href = link.href;
        if (!href) return;

        const cid = extractCID(href);
        if (cid) {
            const linkType = detectLinkType(href);
            const rect = link.getBoundingClientRect();
            copyBtn.style.display = 'block';
            copyBtn.style.top = `${rect.bottom + window.scrollY + 5}px`;
            copyBtn.style.left = `${rect.left + (rect.width / 2) + window.scrollX}px`;

            copyBtn.textContent = `复制 ${linkType}`;
            copyBtn.onclick = () => copyToClipboard(cid, copyBtn);
        }
    });

    // 鼠标移出事件处理
    document.addEventListener('mouseout', function(e) {
        if (!e.target.closest('a') && !e.target.closest('.ipfs-copy-btn')) {
            copyBtn.style.display = 'none';
        }
    });

    // 防止按钮消失得太快
    copyBtn.addEventListener('mouseover', function() {
        copyBtn.style.display = 'block';
    });

    copyBtn.addEventListener('mouseout', function(e) {
        if (!e.relatedTarget || !e.relatedTarget.closest('a')) {
            copyBtn.style.display = 'none';
        }
    });

    // 添加批量按钮的点击事件
    batchCopyBtn.addEventListener('click', batchCopyCIDs);
    batchFilenameBtn.addEventListener('click', batchCopyFilenames); // 添加文件名复制按钮的点击事件
    batchDownloadBtn.addEventListener('click', batchCopyDownloadLinks);
    toggleBtn.addEventListener('click', toggleCollapse);
})();