Copy Magnet Links

Enhanced copy magnet links tool with localStorage persistence, SPA support, and smart duplicate detection

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Copy Magnet Links
// @namespace    https://github.com/
// @version      2.0
// @description  Enhanced copy magnet links tool with localStorage persistence, SPA support, and smart duplicate detection
// @author       Enhanced Copy Magnet Links
// @match        *://*/*
// @grant        GM_setClipboard
// @grant        unsafeWindow
// @license      MIT
// ==/UserScript==


(function() {
    'use strict';

    // Global state management
    var observer = null;
    var button = null;
    var STORAGE_KEY = 'copy-magnet-links-state';

    // Initialize data from localStorage
    function initializeFromStorage() {
        try {
            var stored = localStorage.getItem(STORAGE_KEY);
            if (stored) {
                var data = JSON.parse(stored);
                return {
                    discoveredLinks: new Set(data.discoveredLinks || []),
                    copiedLinks: new Set(data.copiedLinks || []),
                    lastUpdated: data.lastUpdated || Date.now()
                };
            }
        } catch (e) {
            console.warn('Failed to load from localStorage:', e);
        }
        return {
            discoveredLinks: new Set(),
            copiedLinks: new Set(),
            lastUpdated: Date.now()
        };
    }

    // Save state to localStorage
    function saveToStorage(discoveredLinks, copiedLinks) {
        try {
            var data = {
                discoveredLinks: Array.from(discoveredLinks),
                copiedLinks: Array.from(copiedLinks),
                lastUpdated: Date.now(),
                url: window.location.href
            };
            localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
        } catch (e) {
            console.warn('Failed to save to localStorage:', e);
            // If storage is full, clear old data and try again
            try {
                localStorage.removeItem(STORAGE_KEY);
                var data = {
                    discoveredLinks: Array.from(discoveredLinks),
                    copiedLinks: Array.from(copiedLinks),
                    lastUpdated: Date.now(),
                    url: window.location.href
                };
                localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
            } catch (e2) {
                console.warn('LocalStorage is not available or full');
            }
        }
    }

    // Clear old storage data (older than 24 hours)
    function clearOldStorageData() {
        try {
            var stored = localStorage.getItem(STORAGE_KEY);
            if (stored) {
                var data = JSON.parse(stored);
                // Use 24 hours for localStorage since it's more persistent
                if (data.lastUpdated && Date.now() - data.lastUpdated > 86400000) { // 24 hours
                    localStorage.removeItem(STORAGE_KEY);
                    return true;
                }
            }
        } catch (e) {
            console.warn('Failed to clear old storage data:', e);
        }
        return false;
    }

    // Get storage size information
    function getStorageInfo() {
        try {
            var stored = localStorage.getItem(STORAGE_KEY);
            var size = stored ? new Blob([stored]).size : 0;
            return {
                size: size,
                sizeText: size > 1024 ? (size / 1024).toFixed(2) + ' KB' : size + ' bytes',
                itemCount: stored ? JSON.parse(stored).discoveredLinks.length : 0
            };
        } catch (e) {
            return { size: 0, sizeText: '0 bytes', itemCount: 0 };
        }
    }

    // Initialize state from storage
    var state = initializeFromStorage();
    var discoveredLinks = state.discoveredLinks;
    var copiedLinks = state.copiedLinks;

    // Wait for DOM to be ready
    function waitForDOM() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initScript);
        } else {
            initScript();
        }
    }

    // Scan page for magnet links and update state
    function scanForMagnetLinks() {
        var links = document.querySelectorAll('a[href^="magnet:"]');
        var newLinksFound = false;

        links.forEach(function(link) {
            if (!discoveredLinks.has(link.href)) {
                discoveredLinks.add(link.href);
                newLinksFound = true;
            }
        });

        // Save to storage whenever new links are found
        if (newLinksFound) {
            saveToStorage(discoveredLinks, copiedLinks);
        }

        return newLinksFound;
    }

    // Check if there are magnet links on the page
    function checkAndCreateButton() {
        scanForMagnetLinks(); // Initial scan

        if (discoveredLinks.size > 0) {
            createButton();
            observePageChanges();
        }
    }

    // Initialize the script
    function initScript() {
        // Check if button already exists
        if (document.getElementById('copy-magnet-links-btn')) {
            return;
        }

        // Clear old storage data if needed
        clearOldStorageData();

        checkAndCreateButton();

        // Save initial state
        saveToStorage(discoveredLinks, copiedLinks);
    }

    // Create the copy button
    function createButton() {
        // Update button display with link count
        function updateButtonDisplay() {
            var newCount = discoveredLinks.size - copiedLinks.size;
            if (newCount > 0) {
                button.innerHTML = `
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events: none;">
                        <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>
                    <span style="position: absolute; top: -5px; right: -5px; background: #ff4757; color: white; border-radius: 50%; width: 16px; height: 16px; font-size: 10px; display: flex; align-items: center; justify-content: center; font-weight: bold;">${newCount}</span>
                `;
                var storageInfo = getStorageInfo();
            button.title = `Copy ${newCount} magnet link${newCount > 1 ? 's' : ''} (${discoveredLinks.size} total, ${copiedLinks.size} copied)\nStorage: ${storageInfo.sizeText}\nRight-click to clear`;
            } else {
                button.innerHTML = `
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events: none;">
                        <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
                        <polyline points="22 4 12 14.01 9 11.01"></polyline>
                    </svg>
                `;
                button.title = "All magnet links copied";
            }
        }

        // Function to copy magnet links to clipboard
        function copyMagnetLinks() {
            // Get only links that haven't been copied yet
            var linksToCopy = [];
            discoveredLinks.forEach(function(link) {
                if (!copiedLinks.has(link)) {
                    linksToCopy.push(link);
                }
            });

            if (linksToCopy.length === 0) {
                showNotification("All magnet links have been copied!");
                return;
            }

            // Add loading state
            button.disabled = true;
            button.style.cursor = "wait";
            button.innerHTML = `
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events: none; animation: spin 1s linear infinite;">
                    <path d="M21 2v6h-6"></path>
                    <path d="M3 12a9 9 0 0 1 15-6.7L21 8"></path>
                    <path d="M21 12a9 9 0 0 1-15 6.7L3 16"></path>
                    <path d="M3 22v-6h6"></path>
                </svg>
            `;

            var magnetLinks = linksToCopy.join("\n");

            // Simulate async operation for better UX
            setTimeout(function() {
                // Try multiple copy methods for better compatibility
                function tryCopy(text) {
                    // Method 1: Modern Clipboard API (Chrome, Edge, Firefox, Safari 13.1+)
                    if (navigator.clipboard && navigator.clipboard.writeText) {
                        navigator.clipboard.writeText(text).then(function() {
                            handleCopySuccess(linksToCopy);
                        }).catch(function() {
                            fallbackCopy(text);
                        });
                    }
                    // Method 2: Greasemonkey/Tampermonkey API
                    else if (typeof GM_setClipboard !== 'undefined') {
                        GM_setClipboard(magnetLinks);
                        handleCopySuccess(linksToCopy);
                    }
                    // Method 3: Legacy execCommand method
                    else {
                        fallbackCopy(text);
                    }
                }

                function fallbackCopy(text) {
                    try {
                        var textarea = document.createElement("textarea");
                        textarea.value = text;
                        textarea.style.position = "fixed"; // Prevent scrolling to bottom
                        textarea.style.opacity = "0";
                        document.body.appendChild(textarea);
                        textarea.select();
                        textarea.setSelectionRange(0, 99999); // For mobile devices

                        var successful = document.execCommand("copy");
                        document.body.removeChild(textarea);

                        if (successful) {
                            handleCopySuccess(linksToCopy);
                        } else {
                            showNotification("Failed to copy. Please try manually.");
                        }
                        resetButton();
                    } catch (err) {
                        showNotification("Copy failed: " + err.message);
                        resetButton();
                    }
                }

                // Handle successful copy
                function handleCopySuccess(copiedItems) {
                    // Mark links as copied
                    copiedItems.forEach(function(link) {
                        copiedLinks.add(link);
                    });

                    // Save updated state to storage
                    saveToStorage(discoveredLinks, copiedLinks);

                    showNotification(`Copied ${copiedItems.length} magnet link${copiedItems.length > 1 ? 's' : ''}!`);
                    updateButtonDisplay();
                    resetButton();
                }

                tryCopy(magnetLinks);
            }, 300); // Small delay for better UX
        }

        // Reset button to original state
        function resetButton() {
            button.disabled = false;
            button.style.cursor = "pointer";
            updateButtonDisplay();
        }

        // Function to show a notification
        function showNotification(message) {
            var notification = document.createElement("div");
            notification.className = "magnet-notification";
            notification.innerText = message;
            notification.style.backgroundColor = "#333";
            notification.style.color = "#fff";
            notification.style.padding = "10px";
            notification.style.borderRadius = "5px";
            notification.style.zIndex = 1001;
            notification.style.fontSize = "14px";
            notification.style.opacity = "0.9";
            document.body.appendChild(notification);

            // Automatically remove the notification after 2 seconds
            setTimeout(function() {
                if (document.body.contains(notification)) {
                    document.body.removeChild(notification);
                }
            }, 2000);
        }

        // Add CSS animation and styles
        var style = document.createElement('style');
        style.textContent = `
            @keyframes spin {
                from { transform: rotate(0deg); }
                to { transform: rotate(360deg); }
            }

            #copy-magnet-links-btn {
                position: fixed !important;
                bottom: 10px !important;
                right: 10px !important;
                left: auto !important;
                top: auto !important;
                transform: none !important;
            }

            .magnet-notification {
                position: fixed !important;
                bottom: 60px !important;
                right: 10px !important;
                left: auto !important;
                top: auto !important;
                transform: none !important;
            }
        `;
        document.head.appendChild(style);

        // Create a button to trigger the copy action
        button = document.createElement("button");
        button.id = "copy-magnet-links-btn";
        button.style.position = "fixed";
        button.style.bottom = "10px";
        button.style.right = "10px"; // Place the button in the bottom-right corner
        button.style.zIndex = "2147483647"; // Maximum z-index to ensure it's on top
        button.style.width = "32px";
        button.style.height = "32px";
        button.style.padding = "6px";
        button.style.backgroundColor = "rgba(0, 123, 255, 0.9)";
        button.style.color = "white";
        button.style.border = "none";
        button.style.borderRadius = "6px";
        button.style.cursor = "pointer";
        button.style.display = "flex";
        button.style.alignItems = "center";
        button.style.justifyContent = "center";
        button.style.boxShadow = "0 2px 6px rgba(0,0,0,0.3)";
        button.style.transition = "all 0.2s ease";
        button.style.position = "relative"; // For badge positioning

        // Add hover effects
        button.addEventListener("mouseenter", function() {
            if (!button.disabled) {
                button.style.backgroundColor = "rgba(0, 86, 179, 0.95)";
                button.style.transform = "scale(1.1)";
            }
        });

        button.addEventListener("mouseleave", function() {
            if (!button.disabled) {
                button.style.backgroundColor = "rgba(0, 123, 255, 0.9)";
                button.style.transform = "scale(1)";
            }
        });

        document.body.appendChild(button);

        // Bind updateButtonDisplay to button for external access
        button.updateButtonDisplay = updateButtonDisplay;

        // Initialize button display
        updateButtonDisplay();

        // Add event listener for the button
        button.addEventListener("click", function() {
            copyMagnetLinks();
        });

        // Add right-click context menu to clear storage
        button.addEventListener("contextmenu", function(e) {
            e.preventDefault();

            var storageInfo = getStorageInfo();
            var message = 'Clear all stored magnet link data?\n\n' +
                         'Storage info:\n' +
                         '- ' + storageInfo.itemCount + ' links stored\n' +
                         '- Size: ' + storageInfo.sizeText + '\n' +
                         '- This will reset all copied states';

            if (confirm(message)) {
                try {
                    localStorage.removeItem(STORAGE_KEY);
                    discoveredLinks.clear();
                    copiedLinks.clear();

                    // Re-scan current page
                    var newLinksFound = scanForMagnetLinks();
                    updateButtonDisplay();

                    showNotification('Storage cleared! ' + (newLinksFound ? 'Found ' + discoveredLinks.size + ' links on current page.' : 'No links found.'));
                } catch (err) {
                    showNotification('Failed to clear storage: ' + err.message);
                }
            }
        });
    }

    // Observe page changes for dynamic content
    function observePageChanges() {
        // Initial scan will be handled by createButton function

        observer = new MutationObserver(function(mutations) {
            var newLinksFound = false;

            mutations.forEach(function(mutation) {
                if (mutation.type === 'childList') {
                    for (var i = 0; i < mutation.addedNodes.length; i++) {
                        var node = mutation.addedNodes[i];
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            // Check if the node itself is a magnet link
                            if (node.tagName === 'A' && node.href && node.href.startsWith('magnet:')) {
                                if (!discoveredLinks.has(node.href)) {
                                    discoveredLinks.add(node.href);
                                    newLinksFound = true;
                                }
                            }
                            // Check if the node contains magnet links
                            else if (node.querySelectorAll) {
                                var magnetLinks = node.querySelectorAll('a[href^="magnet:"]');
                                magnetLinks.forEach(function(link) {
                                    if (!discoveredLinks.has(link.href)) {
                                        discoveredLinks.add(link.href);
                                        newLinksFound = true;
                                    }
                                });
                            }
                        }
                    }
                }
                // Also handle attribute changes (e.g., href changes)
                else if (mutation.type === 'attributes' && mutation.attributeName === 'href') {
                    var target = mutation.target;
                    if (target.tagName === 'A' && target.href && target.href.startsWith('magnet:')) {
                        if (!discoveredLinks.has(target.href)) {
                            discoveredLinks.add(target.href);
                            newLinksFound = true;
                        }
                    }
                }
            });

            // Update button if new links were found
            if (newLinksFound && button && button.updateButtonDisplay) {
                button.updateButtonDisplay();
            }

            // Create button if it doesn't exist and we have links
            if (discoveredLinks.size > 0 && !document.getElementById('copy-magnet-links-btn')) {
                createButton();
            }
        });

        // Start observing with comprehensive options
        observer.observe(document.body, {
            childList: true,      // Watch for added/removed nodes
            subtree: true,        // Watch all descendants
            attributes: true,     // Watch for attribute changes
            attributeFilter: ['href'] // Specifically watch href changes
        });

        // Also do periodic scans to catch any missed changes
        setInterval(function() {
            var hadNewLinks = scanForMagnetLinks();
            if (hadNewLinks && button && button.updateButtonDisplay) {
                button.updateButtonDisplay();
            }
        }, 2000); // Check every 2 seconds
    }

    // Start the script
    waitForDOM();
})();