您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add a button to save tweets to Raindrop.io with a custom title, description, and tags.
// ==UserScript== // @name OneClick Save to Raindrop.io for Twitter(X) Post // @namespace http://tampermonkey.net/ // @version 1.1 // @description Add a button to save tweets to Raindrop.io with a custom title, description, and tags. // @author Gemini assisted by @ProtoPioneer inspired by Dinomcworld - https://update.gf.qytechs.cn/scripts/482477/One%20Click%20Copy%20Link%20Button%20for%20Twitter%28X%29.user.js // @match https://twitter.com/* // @match https://mobile.twitter.com/* // @match https://tweetdeck.twitter.com/* // @match https://x.com/* // @icon https://www.google.com/s2/favicons?domain=raindrop.io // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // SVG icon for the Raindrop.io save button (floppy disk) const raindropSVG = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-device-floppy" viewBox="0 0 24 24" stroke-width="2" stroke="#71767C" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2" /><path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M14 4l0 4l-6 0l0 -4" /></svg>'; // If you want to use the alternative, more detailed SVG, uncomment the line below and comment the one above: // const raindropSVG = '<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="400.000000pt" height="400.000000pt" viewBox="0 0 400.000000 400.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,400.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none"><path d="M1909 3190 c-92 -11 -223 -48 -299 -85 -90 -42 -192 -114 -184 -127 4 -7 2 -8 -5 -4 -29 19 -189 -177 -252 -309 -37 -76 -69 -181 -83 -271 l-6 -42 43 16 c31 12 78 16 177 17 113 0 146 -4 204 -23 197 -64 346 -190 430 -365 60 -127 59 -117 63 -575 l4 -423 407 3 407 4 85 27 c167 54 305 166 397 322 61 104 100 272 88 384 -28 263 -160 457 -392 576 -66 34 -73 40 -79 74 -15 96 -48 203 -83 276 -63 132 -223 327 -252 309 -7 -4 -9 -3 -5 4 8 14 -100 89 -186 129 -135 63 -343 99 -479 83z"/></g></svg>'; /** * Extracts relevant data (URL, display name, username, tweet text) from a given tweet element. * @param {HTMLElement} tweetElement - The DOM element representing a single tweet. * @returns {object} An object containing the extracted tweet data. */ function extractTweetData(tweetElement) { let originalUrl = null; let displayName = null; let username = null; let tweetText = null; // 1. Extract original tweet URL // Tries to find the link that points to the tweet's unique status page. const linkElement = tweetElement.querySelector('a[href*="/status/"]'); if (linkElement) { let path = linkElement.getAttribute('href'); // If the path is relative (e.g., /username/status/123), prepend the current origin. // Otherwise, it's already a full URL. if (path && !path.startsWith('http')) { originalUrl = `${window.location.origin}${path}`; } else if (path) { originalUrl = path; } } // 2. Extract display name and username // Locates the container that typically holds the user's display name and @handle. const userNameContainer = tweetElement.querySelector('div[data-testid="User-Name"]'); if (userNameContainer) { // Display Name: Often the first `span` within a link that isn't the @handle. const displayNameSpan = userNameContainer.querySelector('a[role="link"] > div > div > span > span'); if (displayNameSpan) { displayName = displayNameSpan.innerText; } // Username: The `span` element containing the @handle, usually with `dir="ltr"`. const usernameSpan = userNameContainer.querySelector('a[role="link"] > div > div > div > span[dir="ltr"]'); if (usernameSpan) { username = usernameSpan.innerText; // E.g., "@username" } } // 3. Extract tweet text // Finds the `div` element that contains the main text content of the tweet. const tweetTextElement = tweetElement.querySelector('div[data-testid="tweetText"]'); if (tweetTextElement) { tweetText = tweetTextElement.innerText; } return { originalUrl, displayName, username, tweetText }; } /** * Constructs the Raindrop.io "add link" URL with a custom formatted title, description, and tags. * The title format is: "User Display Name (@username) on X/Twitter: "Tweet Text..."". * The entire title is truncated to a maximum of 255 characters. * The description indicates the Tweet Text. * Tags include the current host (x.com or twitter.com) and the @username, each tag is separated with comma (,). * @param {object} tweetData - An object containing tweet data (originalUrl, displayName, username, tweetText). * @returns {string|null} The constructed Raindrop.io URL, or null if essential data is missing. */ function constructRaindropUrl(tweetData) { const { originalUrl, displayName, username, tweetText } = tweetData; // Ensure we have the minimum required data to construct a useful link. if (!originalUrl || !tweetText) { console.error("Missing essential tweet data for Raindrop.io URL construction."); return null; } const host = window.location.hostname; // Determine platform name ('X' or 'Twitter') based on the current hostname. const platformName = host.includes('x.com') ? 'X' : 'Twitter'; // Build the prefix of the title: "User Display Name (@username) on X/Twitter:" let titleParts = []; if (displayName) { titleParts.push(displayName); } if (username) { titleParts.push(`(${username})`); } titleParts.push(`on ${platformName}:`); // Join parts and clean up any excessive spaces resulting from missing parts. let fullTitlePrefix = titleParts.join(' ').replace(/\s+/g, ' ').trim(); // Calculate the maximum allowed length for the tweet text part. // The total title length cannot exceed 255 characters. // We subtract the prefix length, plus a small buffer for surrounding quotes and ellipsis. const maxTextLength = 255 - fullTitlePrefix.length - 4; // -4 accounts for " " and "..." let truncatedTweetText = tweetText; // Truncate the tweet text if it exceeds the calculated maximum length. // Ensure maxTextLength is positive to avoid issues with substring. if (tweetText.length > maxTextLength && maxTextLength > 0) { truncatedTweetText = tweetText.substring(0, maxTextLength).trim() + "..."; } // Construct the final title string. const finalTitle = `${fullTitlePrefix} "${truncatedTweetText}"`; // The description will now contain the full tweet text const description = tweetText; // Tags will include the host and the username, comma-separated let tags = [host.includes('x.com') ? 'x.com' : 'twitter.com']; if (username) { // Remove the "@" symbol from the username, convert to lowercase, and replace spaces with dashes for the tag tags.push(username.replace(/^@/, '').toLowerCase().replace(/\s+/g, '-')); } const finalTags = tags.join(','); // Join tags with a comma // Base URL for adding a new link to Raindrop.io. const raindropBase = 'https://app.raindrop.io/add'; // Encode URL, title, description, and tags to be safe for URL parameters. const encodedLink = encodeURIComponent(originalUrl); const encodedTitle = encodeURIComponent(finalTitle); const encodedDescription = encodeURIComponent(description); const encodedTags = encodeURIComponent(finalTags); // Return the complete Raindrop.io URL with new query parameters. return `${raindropBase}?link=${encodedLink}&title=${encodedTitle}&description=${encodedDescription}&tags=${encodedTags}`; } /** * Adds the Raindrop.io save button to each tweet on the page. */ function addRaindropButtonToTweets() { // Select elements that are typically part of the action bar under a tweet. // On normal feeds, it's 'bookmark'. On the bookmarks page, it's 'removeBookmark'. const actionButtonContainers = document.querySelectorAll('button[data-testid="bookmark"], button[data-testid="removeBookmark"]'); actionButtonContainers.forEach(actionButton => { // Find the immediate parent `div` that contains the action button. const parentDiv = actionButton.parentElement; // Traverse up to find the main `article` element representing the tweet. const tweet = parentDiv.closest('article[data-testid="tweet"]'); // Proceed only if a tweet element is found and our button hasn't been added already. if (tweet && !tweet.querySelector('.raindrop-save-button')) { // Create a new `div` element for our custom button. const raindropButton = document.createElement('div'); raindropButton.classList.add('raindrop-save-button'); // Unique class for identification. raindropButton.setAttribute('aria-label', 'Save to Raindrop.io'); raindropButton.setAttribute('role', 'button'); raindropButton.setAttribute('tabindex', '0'); // Apply inline styles to mimic the appearance of existing Twitter action buttons. raindropButton.style.cssText = 'display: flex; align-items: center; justify-content: center; width: 19px; height: 19px; border-radius: 9999px; transition-duration: 0.2s; cursor: pointer;'; raindropButton.innerHTML = raindropSVG; // Insert the SVG icon. // Attach a click event listener to the new button. raindropButton.addEventListener('click', (event) => { event.stopPropagation(); // Prevent the click from propagating to the tweet itself. const tweetData = extractTweetData(tweet); // If a valid original URL is extracted, proceed to construct and open the Raindrop.io URL. if (tweetData.originalUrl) { const raindropUrl = constructRaindropUrl(tweetData); if (raindropUrl) { window.open(raindropUrl, '_blank'); // Open the URL in a new browser tab. console.log('Tweet sent to Raindrop.io:', raindropUrl); } else { console.error('Failed to construct Raindrop.io URL.'); } } else { console.error('Could not extract tweet URL.'); } }); // To maintain the layout and spacing consistent with other action buttons, // clone the parent container of the action button. const buttonContainerClone = parentDiv.cloneNode(true); buttonContainerClone.style.cssText = 'display: flex; align-items: center;'; buttonContainerClone.innerHTML = ''; // Clear the cloned content to insert our button. buttonContainerClone.appendChild(raindropButton); // Add our custom button to the cloned container. // Insert the new button's container next to the original action buttons. parentDiv.parentNode.insertBefore(buttonContainerClone, parentDiv.nextSibling); } }); } // Use a MutationObserver to dynamically add buttons to tweets as they appear in the DOM. // This is essential for single-page applications like Twitter/X where content loads asynchronously. const observer = new MutationObserver(addRaindropButtonToTweets); observer.observe(document.body, { childList: true, subtree: true }); // Initial call to add buttons to any tweets already present when the script first runs. addRaindropButtonToTweets(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址