您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ویراستارِ توییتهای فارسی در X (Twitter)
// ==UserScript== // @name Twitter Virastar Integration // @version 0.1.1 // @description ویراستارِ توییتهای فارسی در X (Twitter) // @homepage https://github.com/Amm1rr/Twitter-Virastar-Integration/ // @namespace amm1rr.com.virastar // @match https://x.com/* // @require https://update.greasyfork.ip-ddns.com/scripts/527228/1538801/Virastar%20Library.js // @grant none // @license MIT // ==/UserScript== /* * توییتر از کتابخانهی Draft.js برای فیلد متنی استفاده میکند که مدیریت State در React را پیچیده میسازد. * تغییر مستقیم مقدار فیلد ممکن است عملکرد کلیدهای Backspace و Delete را مختل کند، * مخصوصاً اگر متن از طریق insertText یا روشهای مشابه تزریق شود. * برای جلوگیری از این مشکل، از DataTransfer (رویداد Paste) بهره میگیریم تا متن را بهشکل صحیح وارد فیلد کنیم * و از تداخل با State داخلی Draft.js پرهیز شود. * * این اسکریپت یکی از روشهای کمدردسر برای ادغام با توییتر (X) است. * در هر جایی که دکمهی Tweet یا Reply (با data-testid) اضافه شود، در صورت وجود فیلد متنی، یک دکمهی «ویراستار» نیز افزوده میگردد. * بدینترتیب تداخلی با ساختار یا ویژگیهای توییتر ایجاد نخواهد شد. */ (function () { "use strict"; // رنگها و ثابتها const COLORS = { GRAY: "#ccc", GREEN: "#28a745", HIGHLIGHT: "#d4f8d4", TRANSPARENT: "transparent", TEXT_HIGHLIGHT: "#302f2f", }; const TRANSITION_STYLE = "background-color 0.5s ease"; const SELECTORS = { // دو حالت دکمهی توییتر: Post و Reply TWEET_BUTTON: '[data-testid="tweetButtonInline"], [data-testid="tweetButton"]', // فیلد متنی اصلی مبتنی بر Draft.js TWEET_FIELD: '[data-testid="tweetTextarea_0"]', }; const TIMING = { PROCESSING_DELAY: 300, TEXT_HIGHLIGHT: 1000, RESET_DELAY: 1250, UI_UPDATE: 100, }; // مراجع سراسری برای مدیریت دکمهی ویراستار و جلوگیری از ساخت تکراری let lastTweetButtonRef = null; let lastVirastarButtonRef = null; // توابع کمکی متداول // تاخیر ساده بر اساس Promise const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // بررسی شروع متن با حروف فارسی const isPersian = (text) => /^[\u0600-\u06FF\u0750-\u077F]/.test(text); // پاککردن فیلد متنی از طریق ClipboardEvent function clearTweetField(tweetField) { const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(tweetField); selection.removeAllRanges(); selection.addRange(range); const dt = new DataTransfer(); dt.setData("text/plain", ""); const pasteEvent = new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData: dt, }); tweetField.dispatchEvent(pasteEvent); } // درج متن تمیزشده در فیلد، با استفاده از DataTransfer برای ناسازگارنشدن با Draft.js function pasteText(tweetField, text) { const dt = new DataTransfer(); dt.setData("text/plain", text); // تبدیل Line Breakها به <br> مطابق با ساختار Draft.js dt.setData("text/html", text.replace(/\n/g, "<br>")); const pasteEvent = new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData: dt, }); tweetField.dispatchEvent(pasteEvent); } // قراردادن کرسر در انتهای فیلد متنی function setCursorToEnd(tweetField) { const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(tweetField); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); } // بروزرسانی فیلد متنی با متن ویراسته و نمایش افکت رنگی async function updateTweetText(processedText) { const tweetField = document.querySelector(SELECTORS.TWEET_FIELD); if (!tweetField) return; tweetField.focus(); clearTweetField(tweetField); await delay(50); pasteText(tweetField, processedText); tweetField.style.transition = TRANSITION_STYLE; tweetField.style.backgroundColor = COLORS.HIGHLIGHT; requestAnimationFrame(() => { setTimeout( () => (tweetField.style.backgroundColor = COLORS.TRANSPARENT), TIMING.TEXT_HIGHLIGHT ); }); await delay(TIMING.UI_UPDATE); setCursorToEnd(tweetField); } /** * ایجاد دکمهی ویراستار کنار دکمهی Tweet/Reply جدید * - اگر دکمهی قبلی از DOM حذف شده باشد، دکمهی ویراستارش را هم پاک میکنیم. * - از ساخت مجدد و تکراری دکمهی ویراستار جلوگیری میکنیم. */ function createVirastarButton(tweetButton) { // اگر دکمهی قبلی وجود داشته ولی از صفحه حذف شده، دکمهی ویراستار آن هم پاک شود if (lastTweetButtonRef && !document.contains(lastTweetButtonRef)) { if (lastVirastarButtonRef && lastVirastarButtonRef.parentElement) { lastVirastarButtonRef.remove(); } lastVirastarButtonRef = null; lastVirastarButtonRef = null; } // اگر این دکمه عیناً همان دکمهی قبلی است، دوباره نساز if (tweetButton === lastTweetButtonRef) { return; } // چک کنیم اگر در والد همین دکمه، ویراستار ساخته شده، تکراری نسازیم if (tweetButton.parentElement.querySelector("#virastar-button")) { return; } // اگر قصد داشتید همیشه فقط یکی بسازید، باید دکمهی قبلی را حذف کنید؛ // اما در اینجا شما میخواهید با بستهشدن دیالوگ، دکمهی قبلی باقی بماند. // بنابراین دکمهی جدید را میسازیم و مرجع آن را حفظ میکنیم lastTweetButtonRef = tweetButton; const editButton = document.createElement("button"); editButton.id = "virastar-button"; editButton.textContent = "ویراستار ✍️"; editButton.disabled = true; editButton.style.cssText = ` margin-left: 10px; padding: 8px 12px; border: none; border-radius: 9999px; background-color: ${COLORS.GRAY}; color: white; cursor: default; font-size: 14px; transition: background-color 0.3s, transform 0.2s; width: 100px; text-align: center; `; lastVirastarButtonRef = editButton; // هماهنگسازی رنگ دکمهی ویراستار با دکمهی اصلی توییتر const tweetButtonStyles = window.getComputedStyle(tweetButton); const tweetButtonBackgroundColor = tweetButtonStyles.backgroundColor; // رویدادها جهت افکت Hover editButton.addEventListener("mouseover", () => { if (!editButton.disabled) { editButton.style.backgroundColor = COLORS.TEXT_HIGHLIGHT; } }); editButton.addEventListener("mouseout", () => { if (!editButton.disabled) { editButton.style.backgroundColor = tweetButtonBackgroundColor; } }); // رویدادها برای فعال/غیرفعال کردن دکمه بر اساس متن فیلد const tweetField = document.querySelector(SELECTORS.TWEET_FIELD); if (tweetField) { let cachedText = ""; const updateButtonState = () => { const text = tweetField.innerText.trim(); cachedText = text; const hasText = text.length > 0; editButton.disabled = !hasText; editButton.style.backgroundColor = hasText ? tweetButtonBackgroundColor : COLORS.GRAY; editButton.style.cursor = hasText ? "pointer" : "default"; if (hasText) { tweetField.style.direction = isPersian(text) ? "rtl" : "ltr"; } }; // گوشدادن به تغییرات محتوای فیلد Draft.js ["input", "keyup", "compositionend", "textInput"].forEach((ev) => { tweetField.addEventListener(ev, updateButtonState); }); // کلیک روی دکمهی ویراستار برای اصلاح متن editButton.addEventListener("click", async () => { if (editButton.disabled) return; editButton.disabled = true; editButton.textContent = "... ⏳"; editButton.style.transform = "scale(0.95)"; editButton.style.cursor = "default"; await delay(TIMING.PROCESSING_DELAY); const processed = new Virastar().cleanup(cachedText); await updateTweetText(processed); editButton.textContent = "✅"; editButton.style.backgroundColor = COLORS.GREEN; await delay(TIMING.RESET_DELAY); editButton.textContent = "ویراستار ✍️"; editButton.style.backgroundColor = tweetButtonBackgroundColor; editButton.disabled = false; editButton.style.transform = "scale(1)"; editButton.style.cursor = "pointer"; }); // مقدار اولیه برای فعال/غیرفعال updateButtonState(); } // افزودن دکمه به والد دکمهی Tweet/Reply tweetButton.parentElement.appendChild(editButton); } /* * MutationObserver: * فقط نودهای جدیدی که در صفحه اضافه میشوند بررسی میکنیم * تا در کل سند جستوجوی تکراری و سنگین انجام نگیرد. */ const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === "childList" && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { // اگر گره اضافهشده مستقیماً دکمهی توییتر باشد if (node.matches?.(SELECTORS.TWEET_BUTTON)) { if (node.offsetParent !== null) { createVirastarButton(node); } } else { // یا اگر در فرزندان آن یک دکمهی توییتر باشد const btn = node.querySelector?.(SELECTORS.TWEET_BUTTON); if (btn && btn.offsetParent !== null) { createVirastarButton(btn); } } } } } } }); // نظارت بر تغییرات در کل صفحه observer.observe(document.body, { childList: true, subtree: true }); })();