您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
快捷分享:点击【快捷分享】按钮,一键复制当前帖子的“标题、分区、标签和页面链接”,方便快速分享到其他地方。复制评论:点击【复制评论】按钮,复制当前帖子下所有已加载的评论内容。如果评论很多,可以先按住 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(); }); })();