Link2Clip

Copy all links from selected text to clipboard using keyboard shortcut (default: Shift+C)

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Link2Clip
// @name:zh-TW   Link2Clip - 連結複製工具
// @namespace    https://github.com/livinginpurple
// @version      1.0
// @license      WTFPL
// @author       livinginpurple
// @description  Copy all links from selected text to clipboard using keyboard shortcut (default: Shift+C)
// @description:zh-TW  使用鍵盤快捷鍵(預設:Shift+C)將選取文字中的所有連結複製到剪貼簿
// @match        http://*/*
// @match        https://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const scriptName = GM_info.script.name;
    const log = (msg) => console.log(`${scriptName}: ${msg}`);
    const warn = (msg) => console.warn(`${scriptName}: ${msg}`);
    const error = (msg) => console.error(`${scriptName}: ${msg}`);

    // Default keyboard shortcut configuration
    const DEFAULT_CONFIG = {
        alt: false,
        ctrl: false,
        shift: true,
        keyCode: 67 // 'C'
    };

    // Load configuration from storage
    let config = {
        alt: GM_getValue('accel.alt', DEFAULT_CONFIG.alt),
        ctrl: GM_getValue('accel.ctrl', DEFAULT_CONFIG.ctrl),
        shift: GM_getValue('accel.shift', DEFAULT_CONFIG.shift),
        keyCode: GM_getValue('accel.key', DEFAULT_CONFIG.keyCode)
    };

    // Save configuration to storage
    const saveConfig = () => {
        Object.entries(config).forEach(([key, value]) => {
            GM_setValue(`accel.${key === 'keyCode' ? 'key' : key}`, value);
        });
    };

    // Get shortcut string representation
    const getShortcutString = ({ alt, ctrl, shift, keyCode }) => {
        const keys = [];
        if (alt) keys.push('Alt');
        if (ctrl) keys.push('Ctrl');
        if (shift) keys.push('Shift');
        keys.push(String.fromCharCode(keyCode));
        return keys.join(' + ');
    };

    // Show configuration dialog
    const showConfigDialog = () => {
        const currentShortcut = getShortcutString(config);
        const newShortcut = prompt(
            `Current shortcut: ${currentShortcut}\n\n` +
            'To set a new shortcut, press the desired key combination in the next prompt.\n' +
            'Click OK to continue, or Cancel to abort.',
            'Click OK then press your desired keys'
        );

        if (newShortcut === null) return;

        alert('Press your desired key combination now...');

        // Temporarily attach a one-time listener
        const tempListener = (e) => {
            e.preventDefault();
            e.stopPropagation();

            const { altKey, ctrlKey, shiftKey, keyCode } = e;
            config = { alt: altKey, ctrl: ctrlKey, shift: shiftKey, keyCode };

            saveConfig();

            const newShortcutStr = getShortcutString(config);
            alert(`Shortcut updated to: ${newShortcutStr}`);
            log(`Shortcut updated to: ${newShortcutStr}`);

            document.removeEventListener('keydown', tempListener, true);
        };
        document.addEventListener('keydown', tempListener, true);
    };

    // Reset to default configuration
    const resetToDefault = () => {
        config = { ...DEFAULT_CONFIG };
        saveConfig();
        alert('Shortcut reset to default: Shift+C');
        log('Shortcut reset to default: Shift+C');
    };

    // Register menu commands for configuration
    GM_registerMenuCommand('⌨️ Configure Keyboard Shortcut', showConfigDialog);
    GM_registerMenuCommand('🔄 Reset to Default (Shift+C)', resetToDefault);

    // Check if pressed keys match configured shortcut
    const acceleratorMatch = ({ keyCode, altKey, ctrlKey, shiftKey }) =>
        keyCode === config.keyCode &&
        altKey === config.alt &&
        ctrlKey === config.ctrl &&
        shiftKey === config.shift;

    // Get links from selection or entire page
    const getLinks = (selectedOnly) => {
        const selection = window.getSelection();
        return Array.from(document.links)
            .filter(link => !selectedOnly || selection.containsNode(link, true))
            .map(link => link.href);
    };

    // Get platform-specific line ending
    const getEOL = () => {
        const platform = navigator.platform.toLowerCase();
        if (platform.includes('win')) return '\r\n';
        if (platform.includes('mac')) return '\r';
        return '\n';
    };

    // Copy text to clipboard using GM_setClipboard
    const copyToClipboard = (text) => {
        try {
            GM_setClipboard(text);
            showNotification('Links copied to clipboard!', 'success');
            log(`Copied ${text.split('\n').length} links to clipboard`);
        } catch (err) {
            error(`Failed to copy to clipboard: ${err?.message ?? err}`);
            showNotification('Failed to copy links to clipboard', 'error');
        }
    };

    // Show visual notification
    const showNotification = (message, type = 'success') => {
        // Remove existing notification if any
        document.getElementById('link2clip-notification')?.remove();

        const notification = Object.assign(document.createElement('div'), {
            id: 'link2clip-notification',
            textContent: message
        });

        const bgColor = type === 'success' ? '#4CAF50' : '#f44336';

        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: ${bgColor};
            color: white;
            padding: 12px 24px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            z-index: 999999;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            font-size: 14px;
            font-weight: 500;
            animation: slideIn 0.3s ease-out;
        `;

        // Add animation styles
        if (!document.getElementById('link2clip-styles')) {
            const style = Object.assign(document.createElement('style'), {
                id: 'link2clip-styles',
                textContent: `
                    @keyframes slideIn {
                        from {
                            transform: translateX(400px);
                            opacity: 0;
                        }
                        to {
                            transform: translateX(0);
                            opacity: 1;
                        }
                    }
                    @keyframes slideOut {
                        from {
                            transform: translateX(0);
                            opacity: 1;
                        }
                        to {
                            transform: translateX(400px);
                            opacity: 0;
                        }
                    }
                `
            });
            document.head.appendChild(style);
        }

        document.body.appendChild(notification);

        // Remove notification after 2.5 seconds
        setTimeout(() => {
            notification.style.animation = 'slideOut 0.3s ease-out';
            setTimeout(() => notification.remove(), 300);
        }, 2500);
    };

    // Extract and copy links
    const extractAndCopyLinks = () => {
        const links = getLinks(true);

        if (links.length === 0) {
            showNotification('No links found in the selected text', 'error');
            return;
        }

        copyToClipboard(links.join(getEOL()));
    };

    // Keyboard event listener
    const onKeyDown = (event) => {
        if (!acceleratorMatch(event)) return;

        const selection = window.getSelection();
        if (selection?.toString().trim()) {
            event.preventDefault();
            event.stopPropagation();
            extractAndCopyLinks();
        }
    };

    // Register event listener
    document.addEventListener('keydown', onKeyDown, false);

    log(`Initialized with shortcut: ${getShortcutString(config)}`);
})();