您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Customizes my own Tweetdeck experience. It's unlikely someone else will enjoy this.
// ==UserScript== // @name Tweetdeck tweaks // @namespace http://tampermonkey.net/ // @description Customizes my own Tweetdeck experience. It's unlikely someone else will enjoy this. // @copyright WTFPL // @source https://github.com/B1773rm4n/Tweetdeck_Greasemonkey // @version 1.10.0 // @author B1773rm4n // @match https://*.twitter.com/* // @connect asuka-shikinami.club // @icon https://icons.duckduckgo.com/ip2/twitter.com.ico // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // ==/UserScript== let arrayListNames; let leftColumnNode, rightColumnNode let postAlreadyseen = []; ////// Flow Control ////// (async function start() { arrayListNames = await returnNamesFromServer() // wait until the page is sufficiently loaded let waitThreeSecs = new Promise((resolve) => setTimeout(resolve, 3000)) await waitThreeSecs if (document.URL.indexOf('https://twitter.com/') > -1) { await showInListTwitter() // watch for changes watchDomChangesObserver() } else if (document.URL.indexOf('https://tweetdeck.twitter.com/' > -1)) { // check if a new element is loaded and do something observeTimelineForNewPosts() // general css changes addStyles() // observer for the fullscreen picture improvements fullScreenModal() // remove unused panels (uBlock origin) removePanels() // initate localStorage array for seenPosts loadLocalStorage() doTweetdeckActions() } else { console.log('cant find domain') } })(); function doTweetdeckActions(newNode) { styleNameOfPost(newNode) removeShowThisthreadTweetdeck(newNode) removeRetweetedTweetdeck(newNode) } async function runWhenReady(readySelector) { return new Promise((resolve, reject) => { var numAttempts = 0; var tryNow = function () { var elem = document.querySelector(readySelector); if (elem) { resolve(elem) } else { numAttempts++; if (numAttempts >= 20) { let message = 'Giving up after 20 attempts. Could not find: ' + readySelector console.warn(message); reject(message) } else { setTimeout(tryNow, 250 * Math.pow(1.1, numAttempts)); } } }; tryNow(); }) } //// Observers ///// function observeTimelineForNewPosts() { [leftColumnNode, rightColumnNode] = document.getElementsByClassName("js-column"); const config = { attributes: false, childList: true, subtree: true }; const callback = (mutations) => { mutations.forEach((element) => { element.addedNodes.forEach((newNode) => { let isNewTweet = newNode.getAttribute("data-drag-type") == "tweet" if (isNewTweet) { console.log(getUserNameFromNode(newNode)) doTweetdeckActions(newNode) sendPostToServer(newNode) } }); }); } const observer = new MutationObserver(callback); observer.observe(leftColumnNode, config); observer.observe(rightColumnNode, config); } function fullScreenModal() { const targetNode = document.getElementById('open-modal'); const config = { attributes: true, childList: false, subtree: false, attributeFilter: ['style'] }; const callback = function (mutationsList, observer) { for (const mutation of mutationsList) { if (mutation.type === 'attributes') { // Check if an image is opened if (document.getElementsByClassName('med-tray js-mediaembed').length > 0 && document.getElementsByClassName('med-tray js-mediaembed')[0].hasChildNodes()) { // make the whole image area as clickable as you would click on the small x document.getElementsByClassName('js-modal-panel mdl s-full med-fullpanel')[0].onclick = function () { document.getElementsByClassName('mdl-dismiss')[0].click() } // remove unecessary elements // view original under the picture modal document.getElementsByClassName('med-origlink')[0].remove() // view flag media under the picture modal document.getElementsByClassName('med-flaglink')[0].remove() } } } }; const observer = new MutationObserver(callback); observer.observe(targetNode, config); } function watchDomChangesObserver() { let currentLocation = document.location.href const domTreeElementToObserve = document.getElementsByTagName('main')[0] const config = { attributes: false, childList: true, subtree: true }; const observer = new MutationObserver((mutationList) => { if (currentLocation !== document.location.href) { // location changed! currentLocation = document.location.href; console.log('location changed!'); showInListTwitter() } }); observer.observe(domTreeElementToObserve, config); } ////// doTweetdeckActions ////// function styleNameOfPost(newNode) { if (newNode) { // clear only new element let element = newNode.querySelectorAll(".username")[0] // cut the name field so the name_id can be seen always let nameField = element.previousSibling.previousSibling nameField.style.display = 'inherit' nameField.style.width = '120px' nameField.style.overflow = 'clip' // color the name_id field if already in list or not let currentlyDisplayedElementName = element.innerHTML let inNameInList = arrayListNames.includes(currentlyDisplayedElementName) if (inNameInList) { element.style.color = "green" } else { element.style.color = "red" } } else { // color whole screen let usernameArray = document.getElementsByClassName('username') for (let index = 1; index < usernameArray.length; index++) { let element = usernameArray[index]; // cut the name field so the name_id can be seen always let nameField = element.previousSibling.previousSibling nameField.style.display = 'inherit' nameField.style.width = '120px' nameField.style.overflow = 'clip' // color the name_id field if already in list or not let currentlyDisplayedElementName = element.innerHTML let inNameInList = arrayListNames.includes(currentlyDisplayedElementName) if (inNameInList) { element.style.color = "green" } else { element.style.color = "red" } } } } function removeShowThisthreadTweetdeck(newNode) { if (newNode) { // clear only new element newNode.querySelector('.js-show-this-thread').remove() } else { // clear whole screen let list = document.getElementsByClassName('js-show-this-thread') for (let index = 1; index < list.length; index++) { let element = list[index]; element.remove() } } } function removeRetweetedTweetdeck(newNode) { if (newNode) { // todo fix single remove retweeted // clear only new element let element = newNode.querySelector('.nbfc') if (!element.classList.length == 4) { // remove retweeted word element.childNodes[2].remove() // remove self retweet mention let accountName = element.parentNode.nextElementSibling.firstElementChild?.children[1].firstElementChild.firstElementChild.innerText if (accountName == element.innerText) { element.remove() } } } else { // clear whole screen let retweetList = document.getElementsByClassName('tweet-context') for (let index = 1; index < retweetList.length; index++) { let element = retweetList[index]; element.childNodes[3].childNodes[2].remove() } // TODO reimplement self retweet mention removal } } ////// External API Call Functions ////// function returnNamesFromServer() { return new Promise((resolve, reject) => GM_xmlhttpRequest({ method: "GET", url: "https://api.seele-00.asuka-shikinami.club/artists", onload: function (response) { let artistsArray = response.responseText.split("\n") resolve(artistsArray) }, onerror: reject })); } function sendPostToServer(newNode) { // - check if it was scanned already // - if already known / scanned -> discard // - if new -> send curl with image url // select from the column root to the individual post (40 elements as result) let rightColumn = document.getElementsByClassName("js-app-columns app-columns horizontal-flow-container without-tweet-drag-handles")[0].children[1] if (rightColumn.contains(newNode)) { let username = getUserNameFromNode(newNode) // check if the artist is in the list let isUsernameInList = arrayListNames.includes(username) // check if we already processed this post let isPostAlreadyseen = isPostAlreadyProcessed(newNode) if (isUsernameInList && !isPostAlreadyseen) { // check amount of images let images = getImageUrlsFromNode(newNode) images.forEach(element => { // if new -> send curl with image url GM_xmlhttpRequest({ method: "POST", url: "http://api.seele-00.asuka-shikinami.club/imageurl", data: element, onload: function (response) { console.log(response.responseText); console.log(username + " " + isUsernameInList); addPostToAlreadyProcessedList(newNode) } }); }); } else { // - if already known / scanned -> discard } } } ////// Single Action Functions ////// function isPostAlreadyProcessed(newNode) { let tweetId = newNode.getAttribute("data-tweet-id") return postAlreadyseen.includes(tweetId) } function addPostToAlreadyProcessedList(newNode) { let tweetId = newNode.getAttribute("data-tweet-id") postAlreadyseen.push(tweetId) postAlreadyseen = [...new Set(postAlreadyseen)]; postAlreadyseen = postAlreadyseen.slice(-100) let persistPosts = JSON.stringify(postAlreadyseen) GM_setValue("postAlreadyseen", persistPosts); } async function showInListTwitter() { // This colors the text of the artist in the timeline into red when he isn't in the known artist list if (document.URL.indexOf('https://twitter.com/') > -1) { let nameElement if (window.location.href.indexOf('status') > 0) { let nameElementTemp = await runWhenReady("div[data-testid='User-Name']") nameElement = nameElementTemp.children[1]?.firstChild?.firstChild?.firstChild?.firstChild?.firstChild } else { let nameElementTemp = await runWhenReady("div[data-testid='UserName']") nameElement = nameElementTemp?.firstChild?.firstChild?.children[1]?.firstChild?.firstChild?.firstChild?.firstChild } let currentlyDisplayedElementName = nameElement.textContent let inNameInList = arrayListNames.includes(currentlyDisplayedElementName) if (inNameInList) { nameElement.style.color = "green" } else { nameElement.style.color = "red" } } } function getImageUrlsFromNode(node) { var images = [] let imageraws = node.querySelectorAll(".js-media-image-link") imageraws.forEach((element) => { let image = element.style.getPropertyValue('background-image') images.push(image.substr(5, image.length - 7)) }); if (images.length > 0) { return images } else { alert("No getImageUrlsFromNode") } } function removePanels() { document.getElementsByClassName("js-column-header js-action-header flex-shrink--0 column-header")[0].remove() document.getElementsByClassName("js-column-header js-action-header flex-shrink--0 column-header")[0].remove() document.getElementsByClassName("js-column-message scroll-none")[0].parentElement.remove() document.getElementsByClassName("js-column-message scroll-none")[0].parentElement.remove() } function loadLocalStorage() { // initate localStorage array for seenPosts let postAlreadyseenString = GM_getValue("postAlreadyseen") if (postAlreadyseenString) { let postAlreadyseenJson = JSON.parse(postAlreadyseenString) postAlreadyseen = Array.from(postAlreadyseenJson) } } ////// Helper Functions ////// function getAllTweetNodes() { return document.getElementsByTagName("article") } function getLeftColumnTweetNodes() { let leftColumnTweetNodes = [] let allTweetNodes = getAllTweetNodes() for (let i = 0; i < allTweetNodes.length; i++) { const element = allTweetNodes[i]; if (leftColumnNode.contains(element)) { leftColumnTweetNodes.push(element) } } return leftColumnTweetNodes } function getRightColumnTweetNodes() { let rightColumnTweetNodes = [] let allTweetNodes = getAllTweetNodes() for (let i = 0; i < allTweetNodes.length; i++) { const element = allTweetNodes[i]; if (rightColumnNode.contains(element)) { rightColumnTweetNodes.push(element) } } return rightColumnTweetNodes } function isInLeftColumn(node) { return leftColumnNode.contains(node) } function isInRightColumn(node) { return rightColumnNode.contains(node) } function getUserNameFromNode(node) { return node.querySelector(".username").innerText } function getUserIdFromNode(node) { return node.querySelector(".username").previousSibling.previousSibling.innerText } ////// CSS Stylesheets ////// function addStyles() { 'use strict'; GM_addStyle(` .med-fullpanel { background-color: transparent !important; box-shadow: 0 !important; } ` ); GM_addStyle(` html.dark .mdl { background-color: transparent !important; box-shadow: none !important; border-radius: 0 !important; } ` ); GM_addStyle(` html.dark .is-condensed .app-content { left: 0px } ` ); GM_addStyle(` .overlay, .ovl { background: transparent !important; } ` ); GM_addStyle(` .mdl-dismiss { visibility: hidden !important; } ` ); GM_addStyle(` .med-tweet { background-color: rgb(21, 32, 43) !important; } ` ); GM_addStyle(` .js-app-columns .app-columns .horizontal-flow-container .without-tweet-drag-handles { padding-left: 0px !important; } ` ); GM_addStyle(` .app-columns { padding-left: 0px !important; padding: 0px !important; } ` ); }