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