Link2Clip

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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)}`);
})();