Twitter X Icon

Change Twitter X Icon

当前为 2023-07-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Twitter X Icon
  3. // @namespace TwitterX
  4. // @match https://twitter.com/*
  5. // @grant none
  6. // @version 0.1.0
  7. // @author CY Fung
  8. // @description Change Twitter X Icon
  9. // @run-at document-start
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13.  
  14. (() => {
  15.  
  16. let mIconUrl = '';
  17. let linkCache = new Map();
  18.  
  19. let waa = new WeakSet();
  20.  
  21. let mDotUrlMap = new Map();
  22.  
  23. const op = {
  24. radius: (canvas)=>Math.round(canvas.width * 0.14),
  25.  
  26. x: (canvas, radius)=> canvas.width - radius * 2 + radius*0.05,
  27.  
  28. y: (canvas, radius)=>0 + radius * 2 - radius*0.3,
  29.  
  30. };
  31.  
  32. function addRedDotToImage(dataUriBase64, op) {
  33. return new Promise((resolve, reject) => {
  34. // Create an image element to load the data URI
  35. const image = new Image();
  36. image.onload = () => {
  37. const canvas = document.createElement('canvas');
  38. canvas.width = image.width;
  39. canvas.height = image.height;
  40.  
  41. const ctx = canvas.getContext('2d');
  42. ctx.drawImage(image, 0, 0);
  43.  
  44. const radius = op.radius(canvas);
  45. const dotX = op.x(canvas, radius);
  46. const dotY = op.y(canvas, radius);
  47.  
  48. // Draw a red dot on the top right corner
  49. ctx.beginPath();
  50. ctx.arc(dotX, dotY, radius, 0, 2 * Math.PI);
  51. ctx.fillStyle = 'red';
  52. ctx.fill();
  53.  
  54. // Convert the canvas back to a data URI base64 string
  55. const revisedDataUriBase64 = canvas.toDataURL();
  56. resolve(revisedDataUriBase64);
  57. };
  58.  
  59. // Set the image source to the provided data URI
  60. image.src = dataUriBase64;
  61. });
  62. }
  63.  
  64.  
  65. function myLink(link, dottable) {
  66.  
  67. if (waa.has(link)) return;
  68. waa.add(link);
  69.  
  70.  
  71. let hrefDtor = Object.getOwnPropertyDescriptor(link.constructor.prototype, 'href');
  72.  
  73. if (!hrefDtor.set || !hrefDtor.get) {
  74. return;
  75. }
  76.  
  77. const getHref = () => {
  78. return hrefDtor.get.call(link)
  79. }
  80.  
  81. let qq = null;
  82.  
  83.  
  84. async function updateURL(hh) {
  85.  
  86.  
  87. console.log('old href', hh, link.getAttribute('has-dot') === 'true')
  88.  
  89. let nurl = mIconUrl;
  90.  
  91. if(nurl && hh){
  92.  
  93. let href = hh;
  94. let isDotted = link.getAttribute('has-dot') === 'true'
  95.  
  96. if (isDotted && !nurl.startsWith('http') ) {
  97. nurl = await addRedDotToImage(nurl, op);
  98. }
  99.  
  100. }
  101.  
  102. if (hh !== nurl && nurl) link.href = nurl;
  103.  
  104.  
  105.  
  106. }
  107.  
  108. function ckk() {
  109. const hh = getHref();
  110. if (qq === hh) return;
  111. qq = hh;
  112. updateURL(hh);
  113. }
  114.  
  115.  
  116. function updateDotState(hh2) {
  117.  
  118. if (hh2 && typeof hh2 =='string' && hh2.startsWith('http')) {
  119. let href = hh2;
  120. let isDotted = false;
  121.  
  122. if (mDotUrlMap.has(href)) isDotted = mDotUrlMap.get(href);
  123. else {
  124.  
  125. if (href.endsWith('/twitter-pip.3.ico')) isDotted = true;
  126. else {
  127.  
  128. let q = /\?[^?.:\/\\]+/.exec(href);
  129. q = q ? q[0] : '';
  130.  
  131. if (q) {
  132. isDotted = true;
  133. }
  134.  
  135. }
  136.  
  137. mDotUrlMap.set(href, isDotted);
  138.  
  139.  
  140. }
  141.  
  142.  
  143. link.setAttribute('has-dot', isDotted ? 'true' : 'false')
  144. }
  145.  
  146. Promise.resolve().then(ckk)
  147.  
  148.  
  149.  
  150. }
  151.  
  152. let hh2 = null;
  153.  
  154. hh2 = getHref();
  155. updateDotState(hh2);
  156.  
  157. Object.defineProperty(link, 'href', {
  158. get() {
  159. return hh2;
  160. },
  161. set(a) {
  162. if (!a || a.startsWith('http')) {
  163. hh2 = a;
  164. updateDotState(hh2);
  165. }
  166. return hrefDtor.set.call(this, a);
  167. }
  168.  
  169. });
  170.  
  171.  
  172.  
  173. document.addEventListener('my-twitter-icon-has-changed',(evt)=>{
  174.  
  175. if(!evt) return;
  176. let detail = evt.detail;
  177.  
  178. if(!detail)return;
  179. let mIconUrl = detail.mIconUrl;
  180. if(!mIconUrl) return;
  181.  
  182.  
  183. link.href = mIconUrl;
  184. console.log('icon changed')
  185.  
  186. Promise.resolve().then(ckk);
  187.  
  188.  
  189.  
  190. },true);
  191.  
  192. }
  193.  
  194. function mIconFn(iconUrl, rel, dottable) {
  195.  
  196.  
  197.  
  198. const selector = `link[rel~="${rel}"]`;
  199. let link = document.querySelector(selector);
  200. if (!link) {
  201.  
  202. /** @type {HTMLLinkElement} */
  203. link = document.createElement("link");
  204. link.rel = `${rel}`;
  205. link.href = iconUrl;
  206. document.head.appendChild(link);
  207. }
  208.  
  209. for (const link of document.querySelectorAll(selector)) {
  210. if(waa.has(link))continue;
  211. myLink(link, dottable);
  212. }
  213.  
  214.  
  215. }
  216.  
  217. function replacePageIcon(iconUrl) {
  218. mIconFn(iconUrl, 'icon', 1)
  219. }
  220.  
  221. function replaceAppIcon(iconUrl) {
  222.  
  223. mIconFn(iconUrl, 'apple-touch-icon', 0);
  224. }
  225.  
  226.  
  227. const addCSS = (href) => {
  228. let p = document.querySelector('style#j8d4f');
  229. if (!p) {
  230. p = document.createElement('style');
  231. p.id = 'j8d4f';
  232. document.head.appendChild(p);
  233. }
  234.  
  235. let newTextContent = `
  236. a[href="/home"][aria-label="Twitter"] * {
  237. pointer-events: none;
  238. }
  239. a[href="/home"][aria-label="Twitter"] > div::before {
  240.  
  241. background-image: url("${href}");
  242.  
  243. position: absolute;
  244. left: 0;
  245. right: 0;
  246. top: 0;
  247. bottom: 0;
  248. content: '';
  249. color: #fff;
  250. display: block;
  251. background-size: cover;
  252. background-position: center;
  253. background-repeat: no-repeat;
  254. border-radius: 50% / 50%;
  255. }
  256. a[href="/home"] svg::before {
  257. display: block;
  258. position: absolute;
  259. content: "";
  260. left: 0;
  261. right: 0;
  262. top: 0;
  263. bottom: 0;
  264. }
  265.  
  266.  
  267. a[href="/home"] svg path {
  268. visibility: collapse;
  269. }
  270.  
  271. `;
  272. newTextContent = newTextContent.trim();
  273.  
  274. if (p.textContent !== newTextContent) p.textContent = newTextContent;
  275. }
  276.  
  277. let qdd = 0;
  278.  
  279. function sendMessageIconChanged (mIconUrl){
  280. document.dispatchEvent(new CustomEvent('my-twitter-icon-has-changed', {detail: { mIconUrl }}));
  281. }
  282.  
  283. function changeIconFn(withPageElement) {
  284. mIconUrl = localStorage.getItem('myCustomTwitterIcon');
  285. if (!mIconUrl) return;
  286.  
  287. let tid = qdd = Date.now();
  288.  
  289. if (tid !== qdd) return;
  290.  
  291. addCSS(mIconUrl);
  292. replacePageIcon(mIconUrl);
  293. replaceAppIcon(mIconUrl);
  294.  
  295. sendMessageIconChanged(mIconUrl)
  296.  
  297.  
  298. }
  299.  
  300.  
  301. function onImageLoaded(dataURL) {
  302.  
  303.  
  304. // Save the data URL to localStorage with a specific key
  305. localStorage.setItem('myCustomTwitterIcon', dataURL);
  306. console.log('myCustomTwitterIcon - done');
  307. changeIconFn(1);
  308.  
  309.  
  310.  
  311. }
  312.  
  313.  
  314. // Function to handle the image drop event
  315. function handleDrop(event) {
  316. if (!event) return;
  317.  
  318. if (!(event.target instanceof HTMLElement)) return;
  319.  
  320. event.preventDefault();
  321. // Check if the target element is the desired anchor with href="/home"
  322. const targetElement = event.target.closest('a[href="/home"][aria-label="Twitter"]');
  323. if (!targetElement) return;
  324.  
  325. // Get the dropped file (assuming only one file is dropped)
  326. const file = event.dataTransfer.files[0];
  327.  
  328. // Check if the dropped file is an image
  329. if (!file || !file.type.startsWith('image/')) return;
  330.  
  331. linkCache.clear();
  332.  
  333. // Read the image file and convert to base64 data URL
  334. let reader = new FileReader();
  335. reader.onload = function () {
  336. Promise.resolve(reader.result).then(onImageLoaded);
  337. reader = null;
  338. };
  339. reader.readAsDataURL(file);
  340. }
  341.  
  342. // Function to handle the dragover event and allow dropping
  343. function handleDragOver(event) {
  344. event.preventDefault();
  345. }
  346.  
  347.  
  348. if (localStorage.getItem('myCustomTwitterIcon')) {
  349.  
  350. changeIconFn(0);
  351. }
  352.  
  353. let observer = null;
  354.  
  355. // Function to check if the target element is available and hook the drag and drop functionality
  356. function hookDragAndDrop() {
  357. const targetElement = document.querySelector('a[href="/home"]');
  358. if (targetElement && observer) {
  359. targetElement.addEventListener('dragover', handleDragOver);
  360. targetElement.addEventListener('drop', handleDrop);
  361. console.log('Drag and drop functionality hooked.');
  362.  
  363.  
  364.  
  365. observer.takeRecords();
  366. // Stop and disconnect the observer since the targetElement is found
  367. observer.disconnect();
  368. observer = null;
  369.  
  370. if (localStorage.getItem('myCustomTwitterIcon')) {
  371.  
  372. changeIconFn(1);
  373. }
  374.  
  375.  
  376. }
  377. }
  378.  
  379. // Use MutationObserver to observe changes in the document
  380. observer = new MutationObserver(function (mutationsList, observer) {
  381. let p = false;
  382. for (const mutation of mutationsList) {
  383. if (mutation.type === 'childList' || mutation.type === 'subtree') {
  384. p = true;
  385.  
  386. }
  387. }
  388. if (p) hookDragAndDrop();
  389. });
  390.  
  391. // Start observing the entire document
  392. observer.observe(document, { childList: true, subtree: true });
  393.  
  394.  
  395.  
  396.  
  397. })();