Magnet Link Extractor

Extract and display magnet links with a single click

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Magnet Link Extractor
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Extract and display magnet links with a single click
// @note         v1.2 updates: 
// @note         - The icon now only appears on pages with magnet links; 
// @note         - Clicking the magnet icon automatically copies all detected magnet links to the clipboard. No additional action is required;
// @note         - The panel now features a selectable design with checkboxes for better user control.
// @match        *://*/*
// @grant        GM_addStyle
// @run-at       document-end
// @license      MIT

// ==/UserScript==

(function () {
    'use strict';

    // Add CSS
    GM_addStyle(`
        #magnet-icon {
            position: fixed;
            top: 50px;
            right: 5px;
            width: 25px;
            height: 25px;
            background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBVcGxvYWRlZCB0bzogU1ZHIFJlcG8sIHd3dy5zdmdyZXBvLmNvbSwgR2VuZXJhdG9yOiBTVkcgUmVwbyBNaXhlciBUb29scyAtLT4NCjxzdmcgaGVpZ2h0PSI4MDBweCIgd2lkdGg9IjgwMHB4IiB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiANCgkgdmlld0JveD0iMCAwIDUxMi4wMTkgNTEyLjAxOSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8cGF0aCBzdHlsZT0iZmlsbDojREE0NDUzOyIgZD0iTTM2NC40MzcsMjIzLjAyNkwyMDguMzczLDM3OS4wNTljLTIwLjc4MSwyMC44MTItNTQuNjI1LDIwLjgxMi03NS40MDYsMA0KCWMtMjAuODEzLTIwLjc4MS0yMC44MTMtNTQuNjI1LDAtNzUuNDA3bDE1Ni4wMzEtMTU2LjA2M0wyMDYuMDMsNjQuNjM1TDQ5Ljk5NywyMjAuNjgybDAsMGMtNjYuNjU3LDY2LjY1Ny02Ni42NTcsMTc0LjcwNSwwLDI0MS4zNDUNCgljNjYuNjU3LDY2LjY1NywxNzQuNzIsNjYuNjU3LDI0MS4zNDUsMGwxNTYuMDY0LTE1Ni4wMzJMMzY0LjQzNywyMjMuMDI2eiIvPg0KPHBvbHlnb24gc3R5bGU9ImZpbGw6I0NDRDFEOTsiIHBvaW50cz0iMTMwLjYyMywxNDAuMDU3IDIxMy41OTIsMjIzLjAyNiAyODguOTk4LDE0Ny41ODggMjA2LjAzLDY0LjYzNSAiLz4NCjxnPg0KCTxwYXRoIHN0eWxlPSJmaWxsOiNBQUIyQkQ7IiBkPSJNNDY2LjIxOSwyMTkuMjQ1YzEuOTM4LTEuOTM4LDMuMTI1LTQuNjA5LDMuMTI1LTcuNTYyYzAtNS44OTEtNC43ODEtMTAuNjU2LTEwLjY1Ni0xMC42NTYNCgkJYy0yLjkzOCwwLTUuNjI1LDEuMTg4LTcuNTYyLDMuMTI1bC0zMC4xNTYsMzAuMTU2bDAsMGMtMS45MzgsMS45MzgtMy4xMjUsNC41OTQtMy4xMjUsNy41NDZjMCw1Ljg5MSw0Ljc4MSwxMC42NzIsMTAuNjU2LDEwLjY3Mg0KCQljMi45NjksMCw1LjYyNS0xLjIwMyw3LjU2Mi0zLjE0MUw0NjYuMjE5LDIxOS4yNDV6Ii8+DQoJPHBhdGggc3R5bGU9ImZpbGw6I0FBQjJCRDsiIGQ9Ik01MDguODc1LDI2NC40OGMtMi4wNjItMi4wNzgtNC44MTItMy4xMjUtNy41MzEtMy4xMjVsMCwwaC00Mi42NTZsMCwwDQoJCWMtMi43MTksMC01LjQ2OSwxLjA0Ny03LjUzMSwzLjEyNWMtNC4xODgsNC4xNzItNC4xODgsMTAuOTIyLDAsMTUuMDc4YzIuMDYyLDIuMDk0LDQuODEyLDMuMTI1LDcuNTMxLDMuMTI1aDQyLjY1Ng0KCQljMi43MTksMCw1LjQ2OS0xLjAzMSw3LjUzMS0zLjEyNUM1MTMuMDYzLDI3NS40MDIsNTEzLjA2MywyNjguNjUyLDUwOC44NzUsMjY0LjQ4eiIvPg0KCTxwYXRoIHN0eWxlPSJmaWxsOiNBQUIyQkQ7IiBkPSJNNDA1Ljg3NSwxNjEuNDc5Yy00LjE1Ni00LjE3Mi0xMC45MDYtNC4xNzItMTUuMDYyLDBjLTIuMDk0LDIuMDc4LTMuMTI1LDQuODEyLTMuMTI1LDcuNTQ3djQyLjY1Nw0KCQljMCwyLjcxOSwxLjAzMSw1LjQ2OSwzLjEyNSw3LjUzMWM0LjE1Niw0LjE4OCwxMC45MDYsNC4xODgsMTUuMDYyLDBjMi4wOTQtMi4wNjIsMy4xMjUtNC44MTIsMy4xMjUtNy41MzFsMCwwdi00Mi42NTdsMCwwDQoJCUM0MDksMTY2LjI5MSw0MDcuOTY5LDE2My41NTcsNDA1Ljg3NSwxNjEuNDc5eiIvPg0KCTxwYXRoIHN0eWxlPSJmaWxsOiNBQUIyQkQ7IiBkPSJNMzA3Ljg3Myw2MC44NjhMMzA3Ljg3Myw2MC44NjhjMS45MzgtMS45MzgsMy4xMjYtNC41OTQsMy4xMjYtNy41MzENCgkJYzAtNS45MDYtNC43ODItMTAuNjcyLTEwLjY4OC0xMC42NzJjLTIuOTM4LDAtNS41OTQsMS4yMDMtNy41MzEsMy4xMjV2LTAuMDE2bC0zMC4xNTYsMzAuMTg4bDAsMA0KCQljLTEuOTM4LDEuOTIyLTMuMTI1LDQuNTk0LTMuMTI1LDcuNTMxYzAsNS44OTEsNC43ODEsMTAuNjcyLDEwLjY1NiwxMC42NzJjMi45MzgsMCw1LjYyNS0xLjIwMyw3LjUzMS0zLjEyNWwwLDBMMzA3Ljg3Myw2MC44Njh6Ig0KCQkvPg0KCTxwYXRoIHN0eWxlPSJmaWxsOiNBQUIyQkQ7IiBkPSJNMzUwLjUzMSwxMDYuMTE5Yy0yLjA5NC0yLjA3OC00LjgxMi0zLjEyNS03LjUzMS0zLjEyNWwwLDBoLTQyLjY1N2wwLDANCgkJYy0yLjc1LDAtNS40NjksMS4wNDctNy41NjIsMy4xMjVjLTQuMTU2LDQuMTcyLTQuMTU2LDEwLjkyMiwwLDE1LjA5NGMyLjA5NCwyLjA3OCw0LjgxMiwzLjEyNSw3LjU2MiwzLjEyNUgzNDMNCgkJYzIuNzE5LDAsNS40MzgtMS4wNDcsNy41MzEtMy4xMjVDMzU0LjY4NywxMTcuMDQxLDM1NC42ODcsMTEwLjI5MSwzNTAuNTMxLDEwNi4xMTl6Ii8+DQoJPHBhdGggc3R5bGU9ImZpbGw6I0FBQjJCRDsiIGQ9Ik0yNDcuNTMsMy4xMThjLTQuMTU2LTQuMTU3LTEwLjkwNi00LjE1Ny0xNS4wOTQsMGMtMi4wNjIsMi4wOTQtMy4xMjUsNC44MTIsMy4xMjUsNy41NjJ2NDIuNjI1DQoJCWMwLDIuNzUsMS4wNjIsNS40NjksMy4xMjUsNy41NjJjNC4xODgsNC4xNTcsMTAuOTM4LDQuMTU3LDE1LjA5NCwwYzIuMDk0LTIuMDk0LDMuMTI1LTQuODEyLDMuMTI1LTcuNTQ3bDAsMFYxMC42NjVsMCwwDQoJCUMyNTAuNjU1LDcuOTMxLDI0OS42MjMsNS4yMTIsMjQ3LjUzLDMuMTE4eiIvPg0KPC9nPg0KPHBvbHlnb24gc3R5bGU9ImZpbGw6I0NDRDFEOTsiIHBvaW50cz0iNDQ3LjQwNiwzMDUuOTk1IDM2NC40MzcsMjIzLjAyNiAyODguOTk4LDI5OC40MzMgMzcxLjk2OCwzODEuNDAzICIvPg0KPC9zdmc+') no-repeat center center;
            background-size: cover;
            cursor: grab;
            z-index: 1000;
            display: none; /* Start hidden */
        }

        #magnet-popup {
            display: none;
            position: fixed;
            bottom: 80px;
            right: 20px;
            width: 300px;
            max-height: 400px;
            background: white;
            border: 1px solid #ccc;
            box-shadow: 0px 0px 10px rgba(0,0,0,0.1);
            overflow-y: auto;
            z-index: 1001;
            padding: 10px;
        }

        #select-options {
            display: flex;
            justify-content: space-between;
            margin-bottom: 10px;
        }

        .button-group {
            display: flex;
            gap: 3px; /* Adjust this value to change button spacing */
        }

        button {
            cursor: pointer;
            padding: 4px 8px; /* Adjust padding for button size */
            border: none;
            border-radius: 3px;
            font-size: 12px; /* Adjust font size */
        }

        #select-all-btn {
            background: #4CAF50;
            color: white;
        }

        #select-none-btn {
            background: #f44336;
            color: white;
        }

        #copy-selected-btn {
            background: #2196F3;
            color: white;
            margin-left: auto; /* Move to the right */
        }

        #copy-feedback {
            font-size: 12px;
            color: green;
            margin-bottom: 0px; /* Space between feedback and links */
            text-align: center;
            display: none;
            font-weight: normal;
        }

        .magnet-count {
            text-align: left; /* Align the count to the left */
            font-size: 12px; /* Adjust the text size */
            color: #666; /* Set the color */
            margin-top: 2px; /* Add spacing above the count */
            margin-bottom: 5px; /* Add spacing below the count */
        }

        .magnet-item {
            display: flex;
            align-items: center; /* Center-align the checkbox and label */
            margin: 0; /* Reset margins */
            font-family: sans-serif; /* Unified font */
            font-size: 12px; /* Adjust text size */
            font-weight: normal; /* Ensure the text is not bold */
            text-decoration: none;
            color: #333; /* Unified color */
            white-space: nowrap; /* Prevent line breaking */
        }

        .magnet-checkbox {
            appearance: auto; /* Ensure the checkbox retains its native appearance */
            margin-right: 8px; /* Space between checkbox and label */
            width: 16px; /* Optional: Set a consistent width */
            height: 16px; /* Optional: Set a consistent height */
            vertical-align: middle; 
        }

        .magnet-item label {
            margin-left: 2px; /* Space between checkbox and link text */
            font-family: sans-serif; /* Set desired font */
            font-size: 12px; /* Unified size */
            font-weight: normal; /* Ensure normal weight */
            color: #333; /* Unified color */
            white-space: nowrap; /* Prevent line breaking */
            line-height: 16px; /* Match the checkbox height */
        }
    `);

    // Create the floating icon
    const icon = document.createElement('div');
    icon.id = 'magnet-icon';
    document.body.appendChild(icon);

    // Create the popup
    const popup = document.createElement('div');
    popup.id = 'magnet-popup';
    popup.innerHTML = `
        <div id="select-options" class="button-group">
            <button id="select-all-btn">All</button>
            <button id="select-none-btn">None</button>
            <div id="copy-feedback" style="flex: 1; text-align: center; display: none;">Links copied!</div>
            <button id="copy-selected-btn">Copy</button>
        </div>
        <div class="magnet-count">Counts: <span id="magnet-count">0</span></div>
        <div id="magnet-list"></div>
    `;
    document.body.appendChild(popup);

    // Extract magnet links
    function extractMagnetLinks() {
        const links = new Set();
        document.querySelectorAll('a[href^="magnet:?xt="]').forEach(link => {
            if (link.href.startsWith('magnet:?xt=')) {
                links.add(link.href);
            }
        });

        const magnetList = document.getElementById('magnet-list');
        magnetList.innerHTML = ''; // Clear existing content

        Array.from(links).forEach((magnet, index) => {
            const item = document.createElement('div');
            item.className = 'magnet-item';

            item.innerHTML = `
                <input type="checkbox" class="magnet-checkbox" id="magnet-${index}" data-magnet="${magnet}" checked>
                <label for="magnet-${index}" title="${magnet}">${magnet}</label>
            `;

            magnetList.appendChild(item);
        });

        const linkCount = links.size;
        document.getElementById('magnet-count').textContent = linkCount;

        // Show the icon only if links are found
        if (linkCount > 0) {
            icon.style.display = 'block';
        } else {
            icon.style.display = 'none';
        }
    }

    // Handle Select All button
    document.getElementById('select-all-btn').addEventListener('click', () => {
        document.querySelectorAll('.magnet-checkbox').forEach(cb => cb.checked = true);
    });

    // Handle Select None button
    document.getElementById('select-none-btn').addEventListener('click', () => {
        document.querySelectorAll('.magnet-checkbox').forEach(cb => cb.checked = false);
    });

    // Copy selected magnets
    document.getElementById('copy-selected-btn').addEventListener('click', () => {
        const selectedLinks = Array.from(document.querySelectorAll('.magnet-checkbox:checked'))
            .map(cb => cb.dataset.magnet)
            .join('\n');

        if (selectedLinks) {
            navigator.clipboard.writeText(selectedLinks).then(() => {
                const feedback = document.getElementById('copy-feedback');
                feedback.style.display = 'block';
                setTimeout(() => feedback.style.display = 'none', 2500);
            }).catch(err => {
                console.error('Failed to copy text: ', err);
            });
        }
    });

    // Popup visibility state
    let isPopupVisible = false;

    // Show or hide popup on icon click
    icon.addEventListener('click', () => {
        isPopupVisible = !isPopupVisible;
        popup.style.display = isPopupVisible ? 'block' : 'none';
        if (isPopupVisible) {
            extractMagnetLinks();
            // Default to select all links and copy them
            document.querySelectorAll('.magnet-checkbox').forEach(cb => cb.checked = true);
            document.getElementById('copy-selected-btn').click();
        }
    });

    // Make the icon draggable
    let isDragging = false;
    let offsetX, offsetY;

    icon.addEventListener('mousedown', (e) => {
        isDragging = true;
        offsetX = e.clientX - parseInt(window.getComputedStyle(icon).left || '0');
        offsetY = e.clientY - parseInt(window.getComputedStyle(icon).top || '0');
        icon.style.cursor = 'grabbing';
        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseup', onMouseUp);
    });

    function onMouseMove(e) {
        if (isDragging) {
            const newX = e.clientX - offsetX;
            const newY = e.clientY - offsetY;

            // Keep icon within viewport bounds
            const maxX = window.innerWidth - icon.offsetWidth;
            const maxY = window.innerHeight - icon.offsetHeight;

            icon.style.left = Math.min(Math.max(0, newX), maxX) + 'px';
            icon.style.top = Math.min(Math.max(0, newY), maxY) + 'px';
        }
    }

    function onMouseUp() {
        isDragging = false;
        icon.style.cursor = 'grab';
        document.removeEventListener('mousemove', onMouseMove);
        document.removeEventListener('mouseup', onMouseUp);
    }

    // Initial extraction
    extractMagnetLinks();
})();