// ==UserScript== // @name share-tweet-copy // @namespace https://screw-hand.com/ // @version 0.2.3 // @description support twitter to copy, easy to share. // @author screw-hand // @match https://twitter.com/* // @icon https://abs.twimg.com/favicons/twitter.3.ico // @grant GM_addStyle // @license MIT // @homepage https://github.com/screw-hand/tampermonkey-user.js // @supportURL https://github.com/screw-hand/tampermonkey-user.js/issues/new // @downloadURL none // ==/UserScript== (function () { 'use strict'; /** * Change Log * * * Version 0.2.2(2024-12-23) * - Add greasyfork userscript header of url. * * Version 0.2.1(2024-12-23) * - Publish the script to greasyfork. * * Version 0.2 (2023-12-13) * - Added styles for the copy-tweet button, including support for dark mode. * - Implemented a tooltip to show the result of the copy action. * - Enhanced dark mode support for better user experience in various lighting conditions. * * Version 0.1 (2023-12-13) * - Initial release. * - Basic functionality to copy tweet text to clipboard with a simple template. */ /** * TODO * 1. copy emoji https://twitter.com/sanxiaozhizi/status/1734793603900485822 * 2. Statistics the pic(both git) / videos total count * 3. support user custom input the share template * * FIXME * 1. cannot copy https://twitter.com/Man_Kei/status/1602787456578985984 * 2. notify about copy failed */ /** * Defines the styles for the copy button. * This includes support for dark mode and styling for various states like hover and focus. */ const copyBtnStyle = ` .copy-tweet-button { --button-bg: #e5e6eb; --button-hover-bg: #d7dbe2; --button-text-color: #4e5969; --button-hover-text-color: #164de5; --button-border-radius: 6px; --button-diameter: 24px; --button-outline-width: 2px; --button-outline-color: #9f9f9f; --tooltip-bg: #1d2129; --toolptip-border-radius: 4px; --tooltip-font-family: JetBrains Mono, Consolas, Menlo, Roboto Mono, monospace; --tooltip-font-size: 12px; --tootip-text-color: #fff; --tooltip-padding-x: 7px; --tooltip-padding-y: 7px; --tooltip-offset: 8px; /* --tooltip-transition-duration: 0.3s; */ } @media (prefers-color-scheme: dark) { .copy-tweet-button { --button-bg: #353434; --button-hover-bg: #464646; --button-text-color: #ccc; --button-outline-color: #999; --button-hover-text-color: #8bb9fe; --tooltip-bg: #f4f3f3; --tootip-text-color: #111; } } .copy-tweet-button { box-sizing: border-box; width: var(--button-diameter); height: var(--button-diameter); margin-left: 8px; border-radius: var(--button-border-radius); background-color: var(--button-bg); color: var(--button-text-color); border: none; cursor: pointer; position: relative; outline: var(--button-outline-width) solid transparent; transition: all 0.2s ease; } .tooltip { position: absolute; opacity: 0; left: calc(100% + var(--tooltip-offset)); top: 50%; transform: translateY(-50%); white-space: nowrap; font: var(--tooltip-font-size) var(--tooltip-font-family); color: var(--tootip-text-color); background: var(--tooltip-bg); padding: var(--tooltip-padding-y) var(--tooltip-padding-x); border-radius: var(--toolptip-border-radius); pointer-events: none; transition: all var(--tooltip-transition-duration) cubic-bezier(0.68, -0.55, 0.265, 1.55); } .tooltip::before { content: attr(data-text-initial); } .tooltip::after { content: ""; width: var(--tooltip-padding-y); height: var(--tooltip-padding-y); background: inherit; position: absolute; top: 50%; left: calc(var(--tooltip-padding-y) / 2 * -1); transform: translateY(-50%) rotate(45deg); z-index: -999; pointer-events: none; } .copy-tweet-button svg { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .checkmark { display: none; } .copy-tweet-button:hover .tooltip, .copy-tweet-button:focus:not(:focus-visible) .tooltip { opacity: 1; visibility: visible; } .copy-tweet-button:focus:not(:focus-visible) .tooltip::before { content: attr(data-text-end); } .copy-tweet-button:focus:not(:focus-visible) .clipboard { display: none; } .copy-tweet-button:focus:not(:focus-visible) .checkmark { display: block; } .copy-tweet-button:hover, .copy-tweet-button:focus { background-color: var(--button-hover-bg); } .copy-tweet-button:active { outline: var(--button-outline-width) solid var(--button-outline-color); } .copy-tweet-button:hover svg { color: var(--button-hover-text-color); } `; /** * Adds styles using the DOM method. * @param {string} cssText - The CSS style text to be added. */ function addStyleWithDOM(cssText) { const styleNode = document.createElement('style') styleNode.appendChild(document.createTextNode(cssText)); (document.querySelector('head') || document.documentElement).appendChild(styleNode) } /** * Adds styles using GM_addStyle and returns whether it was successful. * @param {string} cssText - The CSS style text to be added. * @returns {boolean} Whether the style was successfully added. */ function addStyleWithGM(cssText) { const isGMAddStyleAvailable = typeof GM_addStyle !== 'undefined'; if (isGMAddStyleAvailable) { GM_addStyle(cssText); } return isGMAddStyleAvailable; } // Execute style addition and check if it was successful const resultsOfEnforcement = addStyleWithGM(copyBtnStyle) // If unsuccessful, fallback to adding styles using the DOM method if (!resultsOfEnforcement) { addStyleWithDOM(copyBtnStyle) } /** * Contains functions to extract various pieces of data from a tweet element. */ const tweetDataExtractors = { username: ({ tweetElement }) => tweetElement.querySelector('[dir]').innerText, userid: ({ tweetElement }) => findUserID({ tweetElement }), tweetText: ({ tweetElement }) => tweetElement.querySelector('div[data-testid="tweetText"]').innerText, link: ({ tweetElement }) => 'https://twitter.com' + tweetElement.querySelector('a[href*="/status/"]').getAttribute('href') }; /** * User-defined template for formatting tweet data. */ let userTemplate = `{{username}} ({{userid}}) {{tweetText}} {{link}}`; /** * Adds a copy button to a tweet element. * @param {Object} param - Object containing the tweet element. * @param {Element} param.tweetElement - The tweet element. */ function addCopyButtonToTweet({ tweetElement }) { if (tweetElement.querySelector('.copy-tweet-button')) { return; } let copyButton = document.createElement('button'); copyButton.className = 'copy-tweet-button'; // 添加一个类名以避免重复添加 copyButton.innerHTML = ` ` let nameElement = tweetElement.querySelector('[data-testid="User-Name"]'); if (nameElement) { nameElement.appendChild(copyButton); } copyButton.addEventListener('click', e => handleTweetCopyClick({ e, tweetElement })); } /** * Handles the copy button click event. * @param {Object} param - Object containing the event and tweet element. * @param {Event} param.e - The click event. * @param {Element} param.tweetElement - The tweet element. */ function handleTweetCopyClick({ e, tweetElement }) { e.stopPropagation(); let formattedText = formatTweet({ tweetElement }); copyTextToClipboard(formattedText); } /** * Finds the user ID from a tweet element. * @param {Object} param - Object containing the tweet element. * @param {Element} param.tweetElement - The tweet element. * @returns {string} User ID. */ function findUserID({ tweetElement }) { // 使用您提供的选择器获取匹配的元素 let links = tweetElement.querySelectorAll('a[href^="/"][role="link"][tabindex="-1"]'); let userIDElement = links[links.length - 1]; return userIDElement ? userIDElement.textContent : ''; } /** * Formats the tweet data according to the user-defined template. * @param {Object} param - Object containing the tweet element. * @param {Element} param.tweetElement - The tweet element. * @returns {string} Formatted tweet data. */ function formatTweet({ tweetElement }) { let formatted = userTemplate.replace(/\\{{/g, '{').replace(/\\}}/g, '}'); return formatted.replace(/{{(\w+)}}/g, (match, key) => { if (tweetDataExtractors[key]) { return tweetDataExtractors[key]({ tweetElement }); } return match; }); } /** * Copies text to the clipboard. * @param {string} text - Text to be copied. */ function copyTextToClipboard(text) { navigator.clipboard.writeText(text).then(function () { console.log('Tweet copied to clipboard'); }).catch(function (err) { console.error('Could not copy tweet: ', err); }); } /** * Observes DOM mutations to add a copy button to new tweets. */ let observer = new MutationObserver(function (mutations) { let articles = document.querySelectorAll('article[role="article"]'); articles.forEach(function (tweetElement) { if (!tweetElement.querySelector('.copy-tweet-button')) { addCopyButtonToTweet({ tweetElement }); } }); }); // Start observing observer.observe(document.body, { childList: true, subtree: true }); })();