// ==UserScript==
// @name share-tweet-copy
// @namespace https://screw-hand.com/
// @version 0.2.1
// @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.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 });
})();