Twitter Virastar Integration

ویراستارِ توییت‌های فارسی در X (Twitter)

  1. // ==UserScript==
  2. // @name Twitter Virastar Integration
  3. // @version 0.1.1
  4. // @description ویراستارِ توییت‌های فارسی در X (Twitter)
  5. // @homepage https://github.com/Amm1rr/Twitter-Virastar-Integration/
  6. // @namespace amm1rr.com.virastar
  7. // @match https://x.com/*
  8. // @require https://update.gf.qytechs.cn/scripts/527228/1538801/Virastar%20Library.js
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. /*
  14. * توییتر از کتابخانه‌ی Draft.js برای فیلد متنی استفاده می‌کند که مدیریت State در React را پیچیده می‌سازد.
  15. * تغییر مستقیم مقدار فیلد ممکن است عملکرد کلیدهای Backspace و Delete را مختل کند،
  16. * مخصوصاً اگر متن از طریق insertText یا روش‌های مشابه تزریق شود.
  17. * برای جلوگیری از این مشکل، از DataTransfer (رویداد Paste) بهره می‌گیریم تا متن را به‌شکل صحیح وارد فیلد کنیم
  18. * و از تداخل با State داخلی Draft.js پرهیز شود.
  19. *
  20. * این اسکریپت یکی از روش‌های کم‌دردسر برای ادغام با توییتر (X) است.
  21. * در هر جایی که دکمه‌ی Tweet یا Reply (با data-testid) اضافه شود، در صورت وجود فیلد متنی، یک دکمه‌ی «ویراستار» نیز افزوده می‌گردد.
  22. * بدین‌ترتیب تداخلی با ساختار یا ویژگی‌های توییتر ایجاد نخواهد شد.
  23. */
  24.  
  25. (function () {
  26. "use strict";
  27.  
  28. // رنگ‌ها و ثابت‌ها
  29. const COLORS = {
  30. GRAY: "#ccc",
  31. GREEN: "#28a745",
  32. HIGHLIGHT: "#d4f8d4",
  33. TRANSPARENT: "transparent",
  34. TEXT_HIGHLIGHT: "#302f2f",
  35. };
  36.  
  37. const TRANSITION_STYLE = "background-color 0.5s ease";
  38.  
  39. const SELECTORS = {
  40. // دو حالت دکمه‌ی توییتر: Post و Reply
  41. TWEET_BUTTON:
  42. '[data-testid="tweetButtonInline"], [data-testid="tweetButton"]',
  43. // فیلد متنی اصلی مبتنی بر Draft.js
  44. TWEET_FIELD: '[data-testid="tweetTextarea_0"]',
  45. };
  46.  
  47. const TIMING = {
  48. PROCESSING_DELAY: 300,
  49. TEXT_HIGHLIGHT: 1000,
  50. RESET_DELAY: 1250,
  51. UI_UPDATE: 100,
  52. };
  53.  
  54. // مراجع سراسری برای مدیریت دکمه‌ی ویراستار و جلوگیری از ساخت تکراری
  55. let lastTweetButtonRef = null;
  56. let lastVirastarButtonRef = null;
  57.  
  58. // توابع کمکی متداول
  59.  
  60. // تاخیر ساده بر اساس Promise
  61. const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  62.  
  63. // بررسی شروع متن با حروف فارسی
  64. const isPersian = (text) => /^[\u0600-\u06FF\u0750-\u077F]/.test(text);
  65.  
  66. // پاک‌کردن فیلد متنی از طریق ClipboardEvent
  67. function clearTweetField(tweetField) {
  68. const selection = window.getSelection();
  69. const range = document.createRange();
  70. range.selectNodeContents(tweetField);
  71. selection.removeAllRanges();
  72. selection.addRange(range);
  73.  
  74. const dt = new DataTransfer();
  75. dt.setData("text/plain", "");
  76. const pasteEvent = new ClipboardEvent("paste", {
  77. bubbles: true,
  78. cancelable: true,
  79. clipboardData: dt,
  80. });
  81. tweetField.dispatchEvent(pasteEvent);
  82. }
  83.  
  84. // درج متن تمیزشده در فیلد، با استفاده از DataTransfer برای ناسازگارنشدن با Draft.js
  85. function pasteText(tweetField, text) {
  86. const dt = new DataTransfer();
  87. dt.setData("text/plain", text);
  88. // تبدیل Line Breakها به <br> مطابق با ساختار Draft.js
  89. dt.setData("text/html", text.replace(/\n/g, "<br>"));
  90.  
  91. const pasteEvent = new ClipboardEvent("paste", {
  92. bubbles: true,
  93. cancelable: true,
  94. clipboardData: dt,
  95. });
  96. tweetField.dispatchEvent(pasteEvent);
  97. }
  98.  
  99. // قراردادن کرسر در انتهای فیلد متنی
  100. function setCursorToEnd(tweetField) {
  101. const selection = window.getSelection();
  102. const range = document.createRange();
  103. range.selectNodeContents(tweetField);
  104. range.collapse(false);
  105. selection.removeAllRanges();
  106. selection.addRange(range);
  107. }
  108.  
  109. // بروزرسانی فیلد متنی با متن ویراسته و نمایش افکت رنگی
  110. async function updateTweetText(processedText) {
  111. const tweetField = document.querySelector(SELECTORS.TWEET_FIELD);
  112. if (!tweetField) return;
  113.  
  114. tweetField.focus();
  115. clearTweetField(tweetField);
  116. await delay(50);
  117. pasteText(tweetField, processedText);
  118.  
  119. tweetField.style.transition = TRANSITION_STYLE;
  120. tweetField.style.backgroundColor = COLORS.HIGHLIGHT;
  121. requestAnimationFrame(() => {
  122. setTimeout(
  123. () => (tweetField.style.backgroundColor = COLORS.TRANSPARENT),
  124. TIMING.TEXT_HIGHLIGHT
  125. );
  126. });
  127.  
  128. await delay(TIMING.UI_UPDATE);
  129. setCursorToEnd(tweetField);
  130. }
  131.  
  132. /**
  133. * ایجاد دکمه‌ی ویراستار کنار دکمه‌ی Tweet/Reply جدید
  134. * - اگر دکمه‌ی قبلی از DOM حذف شده باشد، دکمه‌ی ویراستارش را هم پاک می‌کنیم.
  135. * - از ساخت مجدد و تکراری دکمه‌ی ویراستار جلوگیری می‌کنیم.
  136. */
  137. function createVirastarButton(tweetButton) {
  138. // اگر دکمه‌ی قبلی وجود داشته ولی از صفحه حذف شده، دکمه‌ی ویراستار آن هم پاک شود
  139. if (lastTweetButtonRef && !document.contains(lastTweetButtonRef)) {
  140. if (lastVirastarButtonRef && lastVirastarButtonRef.parentElement) {
  141. lastVirastarButtonRef.remove();
  142. }
  143. lastVirastarButtonRef = null;
  144. lastVirastarButtonRef = null;
  145. }
  146.  
  147. // اگر این دکمه عیناً همان دکمه‌ی قبلی است، دوباره نساز
  148. if (tweetButton === lastTweetButtonRef) {
  149. return;
  150. }
  151.  
  152. // چک کنیم اگر در والد همین دکمه، ویراستار ساخته شده، تکراری نسازیم
  153. if (tweetButton.parentElement.querySelector("#virastar-button")) {
  154. return;
  155. }
  156.  
  157. // اگر قصد داشتید همیشه فقط یکی بسازید، باید دکمه‌ی قبلی را حذف کنید؛
  158. // اما در اینجا شما می‌خواهید با بسته‌شدن دیالوگ، دکمه‌ی قبلی باقی بماند.
  159.  
  160. // بنابراین دکمه‌ی جدید را می‌سازیم و مرجع آن را حفظ می‌کنیم
  161. lastTweetButtonRef = tweetButton;
  162.  
  163. const editButton = document.createElement("button");
  164. editButton.id = "virastar-button";
  165. editButton.textContent = "ویراستار ✍️";
  166. editButton.disabled = true;
  167. editButton.style.cssText = `
  168. margin-left: 10px;
  169. padding: 8px 12px;
  170. border: none;
  171. border-radius: 9999px;
  172. background-color: ${COLORS.GRAY};
  173. color: white;
  174. cursor: default;
  175. font-size: 14px;
  176. transition: background-color 0.3s, transform 0.2s;
  177. width: 100px;
  178. text-align: center;
  179. `;
  180.  
  181. lastVirastarButtonRef = editButton;
  182.  
  183. // هماهنگ‌سازی رنگ دکمه‌ی ویراستار با دکمه‌ی اصلی توییتر
  184. const tweetButtonStyles = window.getComputedStyle(tweetButton);
  185. const tweetButtonBackgroundColor = tweetButtonStyles.backgroundColor;
  186.  
  187. // رویدادها جهت افکت Hover
  188. editButton.addEventListener("mouseover", () => {
  189. if (!editButton.disabled) {
  190. editButton.style.backgroundColor = COLORS.TEXT_HIGHLIGHT;
  191. }
  192. });
  193. editButton.addEventListener("mouseout", () => {
  194. if (!editButton.disabled) {
  195. editButton.style.backgroundColor = tweetButtonBackgroundColor;
  196. }
  197. });
  198.  
  199. // رویدادها برای فعال/غیرفعال کردن دکمه بر اساس متن فیلد
  200. const tweetField = document.querySelector(SELECTORS.TWEET_FIELD);
  201. if (tweetField) {
  202. let cachedText = "";
  203. const updateButtonState = () => {
  204. const text = tweetField.innerText.trim();
  205. cachedText = text;
  206. const hasText = text.length > 0;
  207. editButton.disabled = !hasText;
  208. editButton.style.backgroundColor = hasText
  209. ? tweetButtonBackgroundColor
  210. : COLORS.GRAY;
  211. editButton.style.cursor = hasText ? "pointer" : "default";
  212.  
  213. if (hasText) {
  214. tweetField.style.direction = isPersian(text) ? "rtl" : "ltr";
  215. }
  216. };
  217.  
  218. // گوش‌دادن به تغییرات محتوای فیلد Draft.js
  219. ["input", "keyup", "compositionend", "textInput"].forEach((ev) => {
  220. tweetField.addEventListener(ev, updateButtonState);
  221. });
  222.  
  223. // کلیک روی دکمه‌ی ویراستار برای اصلاح متن
  224. editButton.addEventListener("click", async () => {
  225. if (editButton.disabled) return;
  226. editButton.disabled = true;
  227. editButton.textContent = "... ⏳";
  228. editButton.style.transform = "scale(0.95)";
  229. editButton.style.cursor = "default";
  230.  
  231. await delay(TIMING.PROCESSING_DELAY);
  232. const processed = new Virastar().cleanup(cachedText);
  233. await updateTweetText(processed);
  234.  
  235. editButton.textContent = "✅";
  236. editButton.style.backgroundColor = COLORS.GREEN;
  237.  
  238. await delay(TIMING.RESET_DELAY);
  239. editButton.textContent = "ویراستار ✍️";
  240. editButton.style.backgroundColor = tweetButtonBackgroundColor;
  241. editButton.disabled = false;
  242. editButton.style.transform = "scale(1)";
  243. editButton.style.cursor = "pointer";
  244. });
  245.  
  246. // مقدار اولیه برای فعال/غیرفعال
  247. updateButtonState();
  248. }
  249.  
  250. // افزودن دکمه به والد دکمه‌ی Tweet/Reply
  251. tweetButton.parentElement.appendChild(editButton);
  252. }
  253.  
  254. /*
  255. * MutationObserver:
  256. * فقط نودهای جدیدی که در صفحه اضافه می‌شوند بررسی می‌کنیم
  257. * تا در کل سند جست‌وجوی تکراری و سنگین انجام نگیرد.
  258. */
  259. const observer = new MutationObserver((mutationsList) => {
  260. for (const mutation of mutationsList) {
  261. if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
  262. for (const node of mutation.addedNodes) {
  263. if (node.nodeType === Node.ELEMENT_NODE) {
  264. // اگر گره اضافه‌شده مستقیماً دکمه‌ی توییتر باشد
  265. if (node.matches?.(SELECTORS.TWEET_BUTTON)) {
  266. if (node.offsetParent !== null) {
  267. createVirastarButton(node);
  268. }
  269. } else {
  270. // یا اگر در فرزندان آن یک دکمه‌ی توییتر باشد
  271. const btn = node.querySelector?.(SELECTORS.TWEET_BUTTON);
  272. if (btn && btn.offsetParent !== null) {
  273. createVirastarButton(btn);
  274. }
  275. }
  276. }
  277. }
  278. }
  279. }
  280. });
  281.  
  282. // نظارت بر تغییرات در کل صفحه
  283. observer.observe(document.body, { childList: true, subtree: true });
  284. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址