// ==UserScript== // @name Twitterᴾˡᵘˢ (View Twitter Origin Images) // @name:zh-TW Twitterᴾˡᵘˢ // @name:zh-CN Twitterᴾˡᵘˢ // @name:ja Twitterᴾˡᵘˢ // @namespace https://greasyfork.org // @version 0.2.8 // @description Enhance Twitter user experience. Load images in original quality, allow download video from tweets, remove tweets that contain specific hashtags or exceed the maximum limit. // @description:zh-TW 增強Twitter使用者體驗。讀取原始畫質的圖片,允許從推文下載影片,移除包含特定Hashtag或超過最大限制的推文。 // @description:zh-CN 增强Twitter使用者体验。读取原始画质的图片,允许从推文下载影片,移除包含特定Hashtag或超过最大限制的推文。 // @description:ja Twitterのユーザー体験を向上させます。元の品質で画像をロードし、ツイートから動画をダウンロードできるようにし、特定のハッシュタグを含むまたは最大限度を超えるツイートを非表示にします。 // @author Pixmi // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com // @match https://twitter.com/* // @match https://mobile.twitter.com/* // @match https://pbs.twimg.com/media/* // @license AGPL-3.0-or-later // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @downloadURL none // ==/UserScript== // Hide the post if the hashtag exceeds the set number. (If set to 0, it will not be enabled) if (GM_getValue('MAX_HASHTAGS') == undefined) { GM_setValue('MAX_HASHTAGS', 20); } // Hide the post if it contains the following hashtag. (Please include "#" and separate using commas) if (GM_getValue('OUT_HASHTAGS') == undefined) { GM_setValue('OUT_HASHTAGS', '#tag1,#tag2'); } // Change OUT_HASHTAGS type to string if (typeof GM_getValue('OUT_HASHTAGS') == 'object') { GM_setValue('OUT_HASHTAGS', GM_getValue('OUT_HASHTAGS').join(',')); } // Custom style. GM_addStyle(` .video-link-icon:hover { color: rgba(240, 181, 5, 1) !important; } .video-link-icon:hover::after { background: rgba(240, 181, 5, .1); border-radius: 50%; content: " "; width: 38.5px; height: 38.5px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .video-link-icon > svg { width: 1.2em; height: 1.2em }`); (function () { 'use strict'; const URL = window.location.href; const match = URL.match(/https:\/\/(pbs\.twimg\.com\/media\/[a-zA-Z0-9\-\_]+)(\?format=|.)(jpg|jpeg|png|webp)/); // browsing an image URL if (match) { // webp change to jpg if (match[3] == 'webp') match[3] = 'jpg'; // change it to obtain the original quality. if (match[2] == '?format=' || !/name=orig/.test(URL)) { window.location.replace(`https://${match[1]}.${match[3]}?name=orig`); } } // if browsing tweets, activate the observer. if (URL.includes('twitter.com')) { const rootmatch = document.evaluate('//div[@id="react-root"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); const rootnode = rootmatch.singleNodeValue; const MAX_HASHTAGS = GM_getValue('MAX_HASHTAGS'); const OUT_HASHTAGS = GM_getValue('OUT_HASHTAGS').split(','); if (rootnode) { let getVidoLink = (element) => { let react = Object.entries(element).find(el => el[0].startsWith("__reactFiber"))[1]; if (!react) return false; let tweet = react.memoizedProps.children.filter(el => (el || {})._owner).map(el => el._owner.memoizedProps.focalTweet).filter(el => el)[0]; if (!tweet) return false; let video = tweet.extended_entities.media[0].video_info.variants.filter(v => v.content_type == "video/mp4").sort((a, b) => b.bitrate - a.bitrate)[0].url.replace(new RegExp("\\?tag=.*"), ""); if (!video) return false; return video; }; let createPopupLink = (link, group) => { let button = document.createElement('a'); button.classList.add('video-link-icon', 'css-18t94o4', 'css-1dbjc4n', 'r-1777fci', 'r-bt1l66', 'r-1ny4l3l', 'r-bztko3', 'r-lrvibr', 'r-1bwzh9t', 'r-115tad6', 'r-14j79pv'); button.title = 'Open the video link or right-click to save the file.'; button.target= '_blank'; button.href = link; button.innerHTML = ''; let colorItem = group.firstChild.querySelector('div[style*="color"]') || false; if (colorItem) button.style.color = colorItem.style.color; return button; }; let callback = (mutationsList, observer) => { for (let mutation of mutationsList) { let target = mutation.target; if (target.className.includes('css-1dbjc4n')) { let hashtags = Array.from(target.querySelectorAll('.css-901oao > .r-18u37iz'), tag => tag.textContent); let hideCheck = []; if (hashtags.length && target.nodeName == 'ARTICLE') { if (MAX_HASHTAGS > 0) hideCheck.push(hashtags.length >= MAX_HASHTAGS); hideCheck.push(hashtags.some(tag => OUT_HASHTAGS.find(item => item == tag))); if (hideCheck.some(item => item === true)) { target.closest('div[data-testid="cellInnerDiv"] > div').style.display = 'none'; target.remove(); continue; } } let images = target.getElementsByTagName('img'); if (!images.length) continue; for (let i = 0; i < images.length; i++) { let image = images[i]; let imgMatch = image.src.match(/https:\/\/(pbs\.twimg\.com\/media\/[a-zA-Z0-9\-\_]+)(\?format=|.)(jpg|jpeg|png|webp)/); // Image url change to original quality. if (imgMatch) { if (imgMatch[3] == 'webp') imgMatch[3] = 'jpg'; if (imgMatch[2] == '?format=' || !/name=orig/.test(image.src)) { image.src = `https://${imgMatch[1]}.${imgMatch[3]}?name=orig`; } } else // Create download video button if (image.src.includes('video_thumb') && URL.includes('/status/')) { if ('link' in image.dataset) continue; let group = target.closest('article').querySelector('div[id^="id"][role="group"]'); if (!group || group.lastChild.nodeName == 'A') continue; let link = getVidoLink(target.closest('section[role="region"]').parentElement); if (!link) continue; image.dataset.link = link; group.append(createPopupLink(link, group)); } } } } }; const observeConfig = { attributes: true, childList: true, subtree: true }; const observer = new MutationObserver(callback); observer.observe(document.body, observeConfig); } } })();