L 站小工具

快捷分享:点击【快捷分享】按钮,一键复制当前帖子的“标题、分区、标签和页面链接”,方便快速分享到其他地方。复制评论:点击【复制评论】按钮,复制当前帖子下所有已加载的评论内容。如果评论很多,可以先按住 PageDown 键,等评论都刷出来再复制。

// ==UserScript==
// @name         L 站小工具
// @namespace    http://tampermonkey.net/
// @version      0.6
// @description 快捷分享:点击【快捷分享】按钮,一键复制当前帖子的“标题、分区、标签和页面链接”,方便快速分享到其他地方。复制评论:点击【复制评论】按钮,复制当前帖子下所有已加载的评论内容。如果评论很多,可以先按住 PageDown 键,等评论都刷出来再复制。
// @match        https://linux.do/t/topic/*
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
 // @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Constants
    const STORAGE_KEYS = {
        POSITION: 'copyHelperUnitPosition_linuxdo_v2',
        VISIBILITY: 'copyHelperVisibility_linuxdo_v1'
    };
    const COPY_FEEDBACK_DURATION = 1500;
    const ERROR_FEEDBACK_DURATION = 2000;

    // Add styles
    GM_addStyle(`
        .copy-helper-unit {
            position: fixed;
            z-index: 10000;
            background-color: rgba(240, 240, 240, 0.9);
            border-radius: 8px;
            box-shadow: 0 4px 10px rgba(0,0,0,0.2);
            padding: 8px;
            display: flex;
            flex-direction: column;
            gap: 6px;
            user-select: none;
            cursor: grab;
            bottom: 10px; /* Default position */
            right: 10px;  /* Default position */
            transition: opacity 0.3s ease;
        }
        .copy-helper-unit.dragging {
            cursor: grabbing;
            opacity: 0.8;
        }
        .copy-helper-unit.collapsed {
            width: 30px;
            height: 30px;
            padding: 0;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            background-color: rgba(0, 123, 255, 0.8);
            color:white;
        }
        .copy-helper-unit.collapsed .copy-helper-button {
            display: none;
        }
        .copy-helper-unit.collapsed .toggle-collapse {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            color:white;
            background-color: transparent;
        }
        .toggle-collapse {
            align-self: flex-end;
            background-color: transparent;
            color: #555;
            border: none;
            font-size: 16px;
            cursor: pointer;
            padding: 0;
            margin: -5px -5px 0 0;
            width: 20px;
            height: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .toggle-collapse:hover {
            color: #000;
        }
        .copy-helper-button {
            padding: 8px 12px;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 13px;
            transition: background-color 0.3s ease, transform 0.1s ease;
            text-align: center;
            background-color: #007bff;
        }
        .copy-helper-button:hover {
            background-color: #0056b3;
        }
        .copy-helper-button:active {
            transform: scale(0.98);
        }
        .copy-helper-button.copied {
            background-color: #28a745 !important;
        }
        .copy-helper-button.error {
            background-color: #dc3545 !important;
        }
    `);

    /**
     * Makes an element draggable
     * @param {HTMLElement} element - The element to make draggable
     * @param {string} storageKey - Key for storing position in GM storage
     * @returns {Object} - Draggable control object
     */
    function makeDraggable(element, storageKey) {
        let isDragging = false;
        let offsetX, offsetY;
        let wasDraggedInThisSession = false;
        
        // Load saved position
        try {
            const savedPositionJSON = GM_getValue(storageKey);
            if (savedPositionJSON) {
                const savedPosition = JSON.parse(savedPositionJSON);
                if (savedPosition && typeof savedPosition.left === 'string' && typeof savedPosition.top === 'string') {
                    element.style.left = savedPosition.left;
                    element.style.top = savedPosition.top;
                    element.style.right = 'auto';
                    element.style.bottom = 'auto';
                }
            }
        } catch (e) {
            console.error(`Error loading saved position: ${e.message}`);
        }

        // Start drag
        function handleStart(e) {
            // Get clientX/Y from either mouse or touch event
            const clientX = e.clientX || (e.touches && e.touches[0] ? e.touches[0].clientX : 0);
            const clientY = e.clientY || (e.touches && e.touches[0] ? e.touches[0].clientY : 0);
            
            // Prevent dragging if click target is one of the action buttons
            if (e.target === metadataButton || e.target === contentButton) return;
            
            isDragging = true;
            wasDraggedInThisSession = false;
            element.classList.add('dragging');
            
            const rect = element.getBoundingClientRect();
            offsetX = clientX - rect.left;
            offsetY = clientY - rect.top;
            
            if (e.preventDefault) e.preventDefault();
        }
        
        // During drag
        function handleMove(e) {
            if (!isDragging) return;
            
            // Get clientX/Y from either mouse or touch event
            const clientX = e.clientX || (e.touches && e.touches[0] ? e.touches[0].clientX : 0);
            const clientY = e.clientY || (e.touches && e.touches[0] ? e.touches[0].clientY : 0);
            
            wasDraggedInThisSession = true;

            let newX = clientX - offsetX;
            let newY = clientY - offsetY;

            const viewportWidth = window.innerWidth;
            const viewportHeight = window.innerHeight;
            const unitWidth = element.offsetWidth;
            const unitHeight = element.offsetHeight;

            // Keep in viewport bounds
            newX = Math.max(0, Math.min(newX, viewportWidth - unitWidth));
            newY = Math.max(0, Math.min(newY, viewportHeight - unitHeight));

            element.style.left = `${newX}px`;
            element.style.top = `${newY}px`;
            element.style.right = 'auto';
            element.style.bottom = 'auto';
            
            if (e.preventDefault) e.preventDefault();
        }
        
        // End drag
        function handleEnd(e) {
            if (!isDragging) return;
            
            isDragging = false;
            element.classList.remove('dragging');

            if (wasDraggedInThisSession) {
                const currentPosition = {
                    left: element.style.left,
                    top: element.style.top
                };
                GM_setValue(storageKey, JSON.stringify(currentPosition));
            }
        }
        
        // Mouse events
        element.addEventListener('mousedown', handleStart);
        document.addEventListener('mousemove', handleMove);
        document.addEventListener('mouseup', handleEnd);
        
        // Touch events for mobile support
        element.addEventListener('touchstart', handleStart, { passive: false });
        document.addEventListener('touchmove', handleMove, { passive: false });
        document.addEventListener('touchend', handleEnd);
        
        return {
            wasDragged: () => wasDraggedInThisSession,
            cleanup: () => {
                element.removeEventListener('mousedown', handleStart);
                document.removeEventListener('mousemove', handleMove);
                element.removeEventListener('mouseup', handleEnd);
                element.removeEventListener('touchstart', handleStart);
                document.removeEventListener('touchmove', handleMove);
                element.removeEventListener('touchend', handleEnd);
            }
        };
    }

    /**
     * Shows feedback on a button after an action
     * @param {HTMLElement} button - The button to show feedback on
     * @param {string} message - Feedback message
     * @param {string} type - Feedback type ('success' or 'error')
     * @param {number} duration - Duration in ms
     */
    function showButtonFeedback(button, message, type = 'success', duration = COPY_FEEDBACK_DURATION) {
        const originalText = button.textContent;
        button.textContent = message;
        
        if (type === 'success') {
            button.classList.add('copied');
        } else {
            button.classList.add('error');
        }
        
        setTimeout(() => {
            button.textContent = originalText;
            button.classList.remove('copied', 'error');
        }, duration);
    }

    // Create UI elements
    const draggableUnit = document.createElement('div');
    draggableUnit.classList.add('copy-helper-unit');
    
    // Toggle collapse button
    const toggleButton = document.createElement('button');
    toggleButton.classList.add('toggle-collapse');
    toggleButton.innerHTML = '−';
    toggleButton.title = '收起/展开';
    draggableUnit.appendChild(toggleButton);
    
    // Metadata copy button
    const metadataButton = document.createElement('button');
    metadataButton.textContent = '快捷分享';
    metadataButton.classList.add('copy-helper-button');
    metadataButton.title = '复制标题、分类、标签和链接';
    draggableUnit.appendChild(metadataButton);

    // Content copy button
    const contentButton = document.createElement('button');
    contentButton.textContent = '复制评论';
    contentButton.classList.add('copy-helper-button');
    contentButton.title = '复制所有评论内容';
    draggableUnit.appendChild(contentButton);

    // Add to DOM and make draggable
    document.body.appendChild(draggableUnit);
    const draggableControl = makeDraggable(draggableUnit, STORAGE_KEYS.POSITION);
    
    // Load saved visibility state
    try {
        const isCollapsed = GM_getValue(STORAGE_KEYS.VISIBILITY) === 'collapsed';
        if (isCollapsed) {
            draggableUnit.classList.add('collapsed');
            toggleButton.innerHTML = '+';
        }
    } catch (e) {
        console.error(`Error loading visibility state: ${e.message}`);
    }
    
    // Toggle collapse/expand
    toggleButton.addEventListener('click', (e) => {
        if (draggableControl.wasDragged()) { // Don't toggle if a drag just ended
            return;
        }
        e.stopPropagation();
        
        const isCurrentlyCollapsed = draggableUnit.classList.contains('collapsed');
        draggableUnit.classList.toggle('collapsed');
        toggleButton.innerHTML = isCurrentlyCollapsed ? '−' : '+';
        
        GM_setValue(STORAGE_KEYS.VISIBILITY, isCurrentlyCollapsed ? 'expanded' : 'collapsed');
    });

    // Copy metadata (title, category, tags, URL)
    metadataButton.addEventListener('click', (e) => {
        e.stopPropagation();
        if (draggableControl.wasDragged()) return;
        
        try {
            // Cache DOM queries for performance
            const url = window.location.href;
            const topicTitleElement = document.querySelector('.title-wrapper .fancy-title');
            const titleText = topicTitleElement ? topicTitleElement.textContent.trim().replace(/\s+/g, ' ') : '无标题信息';
            
            const tags = Array.from(document.querySelectorAll('a.discourse-tag'))
                .map(tag => tag.textContent.trim())
                .join(', ');
            const tagsText = tags || '无标签';
            
            const categoryElement = document.querySelector(".badge-category__name, .topic-category .badge-category");
            const categoryText = categoryElement ? categoryElement.textContent.trim() : '无分区信息';
            
            const textToCopy = `${titleText}\n【${categoryText}】【${tagsText}】\n${url}`;
            GM_setClipboard(textToCopy);
            
            showButtonFeedback(metadataButton, '已复制!');
        } catch (e) {
            console.error(`Error copying metadata: ${e.message}`);
            showButtonFeedback(metadataButton, '复制失败', 'error');
        }
    });

    // Copy content
    contentButton.addEventListener('click', (e) => {
        e.stopPropagation();
        if (draggableControl.wasDragged()) return;
        
        try {
            const cookedElements = document.querySelectorAll('.cooked');
            
            if (cookedElements.length === 0) {
                showButtonFeedback(contentButton, '无内容可复制', 'error', ERROR_FEEDBACK_DURATION);
                return;
            }
            
            const allContent = Array.from(cookedElements)
                .map(item => item.textContent.trim().replace(/\s+/g, ' '));
            const textToCopy = allContent.join('\n\n');
            
            GM_setClipboard(textToCopy);
            showButtonFeedback(contentButton, '已复制!');
        } catch (e) {
            console.error(`Error copying content: ${e.message}`);
            showButtonFeedback(contentButton, '复制失败', 'error');
        }
    });
    
    // Cleanup on page unload
    window.addEventListener('unload', () => {
        draggableControl.cleanup();
    });
})();