// ==UserScript== // @name Twitter X Icon // @namespace TwitterX // @match https://twitter.com/* // @grant none // @version 0.1.0 // @author CY Fung // @description Change Twitter X Icon // @run-at document-start // @license MIT // @downloadURL none // ==/UserScript== (() => { let mIconUrl = ''; let linkCache = new Map(); let waa = new WeakSet(); let mDotUrlMap = new Map(); const op = { radius: (canvas)=>Math.round(canvas.width * 0.14), x: (canvas, radius)=> canvas.width - radius * 2 + radius*0.05, y: (canvas, radius)=>0 + radius * 2 - radius*0.3, }; function addRedDotToImage(dataUriBase64, op) { return new Promise((resolve, reject) => { // Create an image element to load the data URI const image = new Image(); image.onload = () => { const canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; const ctx = canvas.getContext('2d'); ctx.drawImage(image, 0, 0); const radius = op.radius(canvas); const dotX = op.x(canvas, radius); const dotY = op.y(canvas, radius); // Draw a red dot on the top right corner ctx.beginPath(); ctx.arc(dotX, dotY, radius, 0, 2 * Math.PI); ctx.fillStyle = 'red'; ctx.fill(); // Convert the canvas back to a data URI base64 string const revisedDataUriBase64 = canvas.toDataURL(); resolve(revisedDataUriBase64); }; // Set the image source to the provided data URI image.src = dataUriBase64; }); } function myLink(link, dottable) { if (waa.has(link)) return; waa.add(link); let hrefDtor = Object.getOwnPropertyDescriptor(link.constructor.prototype, 'href'); if (!hrefDtor.set || !hrefDtor.get) { return; } const getHref = () => { return hrefDtor.get.call(link) } let qq = null; async function updateURL(hh) { console.log('old href', hh, link.getAttribute('has-dot') === 'true') let nurl = mIconUrl; if(nurl && hh){ let href = hh; let isDotted = link.getAttribute('has-dot') === 'true' if (isDotted && !nurl.startsWith('http') ) { nurl = await addRedDotToImage(nurl, op); } } if (hh !== nurl && nurl) link.href = nurl; } function ckk() { const hh = getHref(); if (qq === hh) return; qq = hh; updateURL(hh); } function updateDotState(hh2) { if (hh2 && typeof hh2 =='string' && hh2.startsWith('http')) { let href = hh2; let isDotted = false; if (mDotUrlMap.has(href)) isDotted = mDotUrlMap.get(href); else { if (href.endsWith('/twitter-pip.3.ico')) isDotted = true; else { let q = /\?[^?.:\/\\]+/.exec(href); q = q ? q[0] : ''; if (q) { isDotted = true; } } mDotUrlMap.set(href, isDotted); } link.setAttribute('has-dot', isDotted ? 'true' : 'false') } Promise.resolve().then(ckk) } let hh2 = null; hh2 = getHref(); updateDotState(hh2); Object.defineProperty(link, 'href', { get() { return hh2; }, set(a) { if (!a || a.startsWith('http')) { hh2 = a; updateDotState(hh2); } return hrefDtor.set.call(this, a); } }); document.addEventListener('my-twitter-icon-has-changed',(evt)=>{ if(!evt) return; let detail = evt.detail; if(!detail)return; let mIconUrl = detail.mIconUrl; if(!mIconUrl) return; link.href = mIconUrl; console.log('icon changed') Promise.resolve().then(ckk); },true); } function mIconFn(iconUrl, rel, dottable) { const selector = `link[rel~="${rel}"]`; let link = document.querySelector(selector); if (!link) { /** @type {HTMLLinkElement} */ link = document.createElement("link"); link.rel = `${rel}`; link.href = iconUrl; document.head.appendChild(link); } for (const link of document.querySelectorAll(selector)) { if(waa.has(link))continue; myLink(link, dottable); } } function replacePageIcon(iconUrl) { mIconFn(iconUrl, 'icon', 1) } function replaceAppIcon(iconUrl) { mIconFn(iconUrl, 'apple-touch-icon', 0); } const addCSS = (href) => { let p = document.querySelector('style#j8d4f'); if (!p) { p = document.createElement('style'); p.id = 'j8d4f'; document.head.appendChild(p); } let newTextContent = ` a[href="/home"][aria-label="Twitter"] * { pointer-events: none; } a[href="/home"][aria-label="Twitter"] > div::before { background-image: url("${href}"); position: absolute; left: 0; right: 0; top: 0; bottom: 0; content: ''; color: #fff; display: block; background-size: cover; background-position: center; background-repeat: no-repeat; border-radius: 50% / 50%; } a[href="/home"] svg::before { display: block; position: absolute; content: ""; left: 0; right: 0; top: 0; bottom: 0; } a[href="/home"] svg path { visibility: collapse; } `; newTextContent = newTextContent.trim(); if (p.textContent !== newTextContent) p.textContent = newTextContent; } let qdd = 0; function sendMessageIconChanged (mIconUrl){ document.dispatchEvent(new CustomEvent('my-twitter-icon-has-changed', {detail: { mIconUrl }})); } function changeIconFn(withPageElement) { mIconUrl = localStorage.getItem('myCustomTwitterIcon'); if (!mIconUrl) return; let tid = qdd = Date.now(); if (tid !== qdd) return; addCSS(mIconUrl); replacePageIcon(mIconUrl); replaceAppIcon(mIconUrl); sendMessageIconChanged(mIconUrl) } function onImageLoaded(dataURL) { // Save the data URL to localStorage with a specific key localStorage.setItem('myCustomTwitterIcon', dataURL); console.log('myCustomTwitterIcon - done'); changeIconFn(1); } // Function to handle the image drop event function handleDrop(event) { if (!event) return; if (!(event.target instanceof HTMLElement)) return; event.preventDefault(); // Check if the target element is the desired anchor with href="/home" const targetElement = event.target.closest('a[href="/home"][aria-label="Twitter"]'); if (!targetElement) return; // Get the dropped file (assuming only one file is dropped) const file = event.dataTransfer.files[0]; // Check if the dropped file is an image if (!file || !file.type.startsWith('image/')) return; linkCache.clear(); // Read the image file and convert to base64 data URL let reader = new FileReader(); reader.onload = function () { Promise.resolve(reader.result).then(onImageLoaded); reader = null; }; reader.readAsDataURL(file); } // Function to handle the dragover event and allow dropping function handleDragOver(event) { event.preventDefault(); } if (localStorage.getItem('myCustomTwitterIcon')) { changeIconFn(0); } let observer = null; // Function to check if the target element is available and hook the drag and drop functionality function hookDragAndDrop() { const targetElement = document.querySelector('a[href="/home"]'); if (targetElement && observer) { targetElement.addEventListener('dragover', handleDragOver); targetElement.addEventListener('drop', handleDrop); console.log('Drag and drop functionality hooked.'); observer.takeRecords(); // Stop and disconnect the observer since the targetElement is found observer.disconnect(); observer = null; if (localStorage.getItem('myCustomTwitterIcon')) { changeIconFn(1); } } } // Use MutationObserver to observe changes in the document observer = new MutationObserver(function (mutationsList, observer) { let p = false; for (const mutation of mutationsList) { if (mutation.type === 'childList' || mutation.type === 'subtree') { p = true; } } if (p) hookDragAndDrop(); }); // Start observing the entire document observer.observe(document, { childList: true, subtree: true }); })();