Guest User

X (Twitter)- Block users on Twitter in one click instead of 3.

a guest
Aug 20th, 2025
5
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 48.25 KB | Source Code | 0 0
  1. // ==UserScript==
  2. // @name Twitter 1-click blocker
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1.1
  5. // @description Block users on Twitter in one click instead of 3. Block from any page where users are listed (/retweets, /followers, etc)!
  6. // @author Dara Oladosu (przerobiony na skrypt użytkownika)
  7. // @match https://*.x.com/*
  8. // @match https://*.twitter.com/*
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_openInTab
  12. // @grant GM_addStyle
  13. // @grant GM_registerMenuCommand
  14. // @grant GM_addValueChangeListener
  15. // @grant window.close
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // Konfiguracja (z index.js)
  22. const CONFIG = {
  23. colors: {
  24. "#000000": "rgb(113, 118, 123)",
  25. "#15202B": "rgb(139, 152, 165)",
  26. "#FFFFFF": "rgb(83, 100, 113)",
  27. },
  28. selectors: {
  29. username: '[data-testid="SideNav_AccountSwitcher_Button"]',
  30. userActions: '[data-testid="userActions"]',
  31. confirmButton: '[data-testid="confirmationSheetConfirm"]',
  32. menuCaret: '[data-testid="caret"]',
  33. userName: '[data-testid="User-Name"]',
  34. grokActions: '[aria-label="Grok actions"]',
  35. blockButton: '[data-testid="block"]',
  36. confirmationDialog: '[data-testid="confirmationSheetDialog"]',
  37. userCell: '[data-testid="UserCell"]',
  38. cellInnerDiv: '[data-testid="cellInnerDiv"]',
  39. primaryColumn: '[data-testid="primaryColumn"]',
  40. placementTracking: '[data-testid="placementTracking"]',
  41. followButton: '[data-testid*="-follow"]',
  42. tweet: '[data-testid="tweet"]',
  43. hoverCard: '[data-testid="HoverCard"]'
  44. },
  45. classes: {
  46. blockButton: 'sw-block-button',
  47. blockButtonWrapper: 'sw-block-button-wrapper',
  48. blockButtonBackground: 'sw-block-button-background',
  49. confirmationClicked: 'sw-block-confirmation-clicked',
  50. menu: 'sw-menu',
  51. blockIcon: 'sw-block-icon',
  52. tooltipText: 'sw-tooltip-block-text'
  53. },
  54. styles: {
  55. errorWrapper: {
  56. backgroundColor: "red",
  57. color: "white",
  58. textAlign: "center",
  59. padding: "10px",
  60. borderRadius: "5px",
  61. margin: "20px",
  62. position: "fixed",
  63. top: "10px",
  64. left: "50%",
  65. transform: "translateX(-50%)",
  66. zIndex: "9999"
  67. }
  68. }
  69. };
  70.  
  71. // Domyślne ustawienia (z options.js i settings.js)
  72. const DEFAULT_SETTINGS = {
  73. action: 'block',
  74. enabledPages: {
  75. timeline: true,
  76. followers: true,
  77. following: true,
  78. search: true,
  79. communities: true,
  80. engagements: true,
  81. status: true,
  82. trending: true,
  83. notifications: true,
  84. hashtag: true,
  85. profile: true,
  86. explore: true
  87. },
  88. showSettingsIcon: true,
  89. stats: {
  90. totalBlocks: 0,
  91. totalMutes: 0,
  92. blockedUsers: [],
  93. mutedUsers: []
  94. }
  95. };
  96.  
  97. // Klasa do zarządzania ustawieniami, używająca GM_setValue/GM_getValue (z settings.js)
  98. class SettingsManager {
  99. constructor() {
  100. this.settings = DEFAULT_SETTINGS;
  101. this.loadSettings();
  102. }
  103.  
  104. async loadSettings() {
  105. this.settings = await GM_getValue('settings', DEFAULT_SETTINGS);
  106. // Scalanie w przypadku braku nowych kluczy w zapisanych ustawieniach
  107. this.settings = {
  108. ...DEFAULT_SETTINGS,
  109. ...this.settings,
  110. enabledPages: { ...DEFAULT_SETTINGS.enabledPages, ...(this.settings.enabledPages || {}) },
  111. stats: { ...DEFAULT_SETTINGS.stats, ...(this.settings.stats || {}) }
  112. };
  113. return this.settings;
  114. }
  115.  
  116. async saveSettings(newSettings) {
  117. await GM_setValue('settings', newSettings);
  118. this.settings = newSettings;
  119. }
  120. }
  121.  
  122. // Klasy interfejsu użytkownika (z index.js)
  123. class BlockButtonUI {
  124. constructor(bgColor, color) {
  125. this.bgColor = bgColor;
  126. this.color = color;
  127. }
  128.  
  129. create(tweetUsername, menuOptionsButton, action = 'block') {
  130. const wrapper = this.createWrapper(menuOptionsButton);
  131. const blockButton = this.createButton(action);
  132. const background = this.createBackground();
  133.  
  134. wrapper.dataset.username = tweetUsername;
  135. wrapper.dataset.action = action;
  136. wrapper.appendChild(background);
  137. wrapper.appendChild(blockButton);
  138.  
  139. this.addHoverEffects(blockButton, background, action);
  140.  
  141. return wrapper;
  142. }
  143.  
  144. createWrapper(menuOptionsButton) {
  145. const grokButton = menuOptionsButton.closest("article")
  146. .querySelector(CONFIG.selectors.grokActions);
  147.  
  148. const wrapper = document.createElement("div");
  149. wrapper.classList.add(CONFIG.classes.blockButtonWrapper);
  150. Object.assign(wrapper.style, {
  151. display: "grid",
  152. position: "absolute",
  153. borderRadius: "100%",
  154. top: "-8px",
  155. right: grokButton ? "45px" : "15px",
  156. placeItems: "center",
  157. gridTemplateAreas: '"icon"',
  158. gridTemplateColumns: "1fr",
  159. marginRight: "15px"
  160. });
  161.  
  162. return wrapper;
  163. }
  164.  
  165. createButton(action = 'block') {
  166. const button = document.createElement("div");
  167. button.classList.add(CONFIG.classes.blockButton);
  168. Object.assign(button.style, {
  169. gridArea: "icon",
  170. cursor: "pointer",
  171. textAlign: "center",
  172. borderRadius: "100%",
  173. backgroundColor: this.bgColor,
  174. lineHeight: "10px",
  175. transition: "all 0.1s ease-in-out",
  176. boxSizing: "border-box",
  177. display: "flex",
  178. alignItems: "center",
  179. justifyContent: "center",
  180. placeSelf: "center"
  181. });
  182.  
  183. button.innerHTML = this.createSVGIcon(action);
  184. return button;
  185. }
  186.  
  187. createBackground() {
  188. const background = document.createElement("div");
  189. background.classList.add(CONFIG.classes.blockButtonBackground);
  190. Object.assign(background.style, {
  191. gridArea: "icon",
  192. backgroundColor: this.bgColor,
  193. borderRadius: "100%",
  194. height: "33px",
  195. width: "33px",
  196. zIndex: "-1",
  197. transition: "all 0.1s ease-in-out",
  198. boxSizing: "border-box"
  199. });
  200.  
  201. return background;
  202. }
  203.  
  204. createSVGIcon(action = 'block') {
  205. return `<svg style="transition: all 0.1s ease-in-out" class="${CONFIG.classes.blockIcon}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" width="16" height="16" fill="${this.bgColor}">
  206. <circle cx="36" cy="36" r="29" stroke="${this.color}" stroke-width="7"/>
  207. <line x1="17" y1="17" x2="55" y2="55" stroke="${this.color}" stroke-width="7" />
  208. </svg>`;
  209. }
  210.  
  211. addHoverEffects(button, background, action = 'block') {
  212. const handleEnter = () => {
  213. Object.assign(button.style, {
  214. backgroundColor: "rgba(255,0,64, 0.1)",
  215. transform: "rotate(270deg)"
  216. });
  217. Object.assign(background.style, {
  218. backgroundColor: "rgba(255,0,64, 0.1)"
  219. });
  220.  
  221. const icon = button.querySelector(`.${CONFIG.classes.blockIcon}`);
  222. const [circle, line] = [icon.querySelector("circle"), icon.querySelector("line")];
  223. circle.style.stroke = "rgb(255,0,64)";
  224. line.style.stroke = "rgb(255,0,64)";
  225. icon.style.fill = "rgb(255,0,64, 0.1)";
  226.  
  227. const username = button.closest(`.${CONFIG.classes.blockButtonWrapper}`)?.dataset.username;
  228. if (username) {
  229. this.addBlockButtonHoverTitle(button, username, false, action);
  230. }
  231. };
  232.  
  233. const handleLeave = () => {
  234. Object.assign(button.style, {
  235. backgroundColor: this.bgColor,
  236. transform: "rotate(0deg)"
  237. });
  238. Object.assign(background.style, {
  239. backgroundColor: this.bgColor
  240. });
  241.  
  242. const icon = button.querySelector(`.${CONFIG.classes.blockIcon}`);
  243. const [circle, line] = [icon.querySelector("circle"), icon.querySelector("line")];
  244. circle.style.stroke = this.color;
  245. line.style.stroke = this.color;
  246. icon.style.fill = this.bgColor;
  247.  
  248. const tooltip = document.querySelector(`#${CONFIG.classes.tooltipText}`);
  249. tooltip?.remove();
  250. };
  251.  
  252. button.addEventListener("mouseenter", handleEnter);
  253. button.addEventListener("mouseleave", handleLeave);
  254. background.addEventListener("mouseenter", handleEnter);
  255. background.addEventListener("mouseleave", handleLeave);
  256. }
  257.  
  258. addBlockButtonHoverTitle(button, username, isProfile = false, action = 'block') {
  259. let hasAddedBlockText = document.querySelector(`#${CONFIG.classes.tooltipText}`);
  260. if (hasAddedBlockText) {
  261. return;
  262. }
  263.  
  264. const actionText = action.charAt(0).toUpperCase() + action.slice(1);
  265.  
  266. let titleParent = document.createElement("div");
  267. titleParent.id = CONFIG.classes.tooltipText;
  268. Object.assign(titleParent.style, {
  269. display: "flex",
  270. paddingInline: "4px",
  271. position: "fixed",
  272. fontSize: "11px",
  273. backgroundColor: "rgba(91, 112, 131)",
  274. alignItems: "center",
  275. justifyContent: "center",
  276. pointerEvents: "none",
  277. minHeight: "20px",
  278. borderRadius: "2px",
  279. zIndex: "10000",
  280. color: "#fff",
  281. fontFamily: "'TwitterChirp', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
  282. });
  283. titleParent.innerHTML = `<span>${actionText} ${username}</span>`;
  284.  
  285. let blockButtonPosition = button.getBoundingClientRect();
  286. titleParent.style.top = `${blockButtonPosition.top + 42}px`;
  287.  
  288. if (isProfile) {
  289. titleParent.style.left = `calc(50% + ${blockButtonPosition.left}px - 50% + 20px)`;
  290. } else {
  291. titleParent.style.left = `calc(50% + ${blockButtonPosition.left}px - 50% + 15px)`;
  292. }
  293.  
  294. titleParent.style.transform = "translateX(-50%)";
  295. document.body.appendChild(titleParent);
  296. }
  297. }
  298.  
  299. class ProfileBlockButtonUI extends BlockButtonUI {
  300. constructor(bgColor, color, settingsManager) {
  301. super(bgColor, color);
  302. this.settingsManager = settingsManager;
  303. }
  304.  
  305. create(page) {
  306. const button = document.createElement("div");
  307. Object.assign(button.style, {
  308. transition: "all 0.1s ease-in-out",
  309. cursor: "pointer",
  310. textAlign: "center",
  311. borderRadius: "100%",
  312. position: "relative",
  313. backgroundColor: this.bgColor,
  314. boxSizing: "border-box",
  315. display: "flex",
  316. alignItems: "center",
  317. justifyContent: "center",
  318. placeSelf: "center",
  319. marginLeft: "5px",
  320. width: "40px",
  321. height: "40px"
  322. });
  323.  
  324. if (page !== "engagements-page") {
  325. button.style.marginBottom = "6px";
  326. }
  327.  
  328. button.innerHTML = this.createSVGIcon();
  329. this.addHoverEffects(button);
  330.  
  331. return button;
  332. }
  333.  
  334. addHoverEffects(button) {
  335. button.addEventListener("mouseenter", async () => {
  336. Object.assign(button.style, {
  337. backgroundColor: "rgba(255,0,64, 0.1)",
  338. transform: "rotate(270deg)"
  339. });
  340.  
  341. const icon = button.querySelector(`.${CONFIG.classes.blockIcon}`);
  342. const [circle, line] = [icon.querySelector("circle"), icon.querySelector("line")];
  343. circle.style.stroke = "rgb(255,0,64)";
  344. line.style.stroke = "rgb(255,0,64)";
  345. icon.style.fill = "rgb(255,0,64, 0.1)";
  346.  
  347. let username;
  348. const parentDiv = button.closest('[data-testid="cellInnerDiv"]');
  349. if (parentDiv) {
  350. const userLink = parentDiv.querySelector('a[href^="/"]');
  351. if (userLink) {
  352. username = userLink.getAttribute('href').substring(1);
  353. }
  354. } else {
  355. username = location.pathname.split("/")[1];
  356. }
  357.  
  358. if (username) {
  359. const settings = await this.settingsManager.loadSettings();
  360. const action = settings.action || 'block';
  361. this.addBlockButtonHoverTitle(button, "@" + username, !parentDiv, action);
  362. }
  363. });
  364.  
  365. button.addEventListener("mouseleave", () => {
  366. Object.assign(button.style, {
  367. backgroundColor: this.bgColor,
  368. transform: "rotate(0deg)"
  369. });
  370.  
  371. const icon = button.querySelector(`.${CONFIG.classes.blockIcon}`);
  372. const [circle, line] = [icon.querySelector("circle"), icon.querySelector("line")];
  373. circle.style.stroke = this.color;
  374. line.style.stroke = this.color;
  375. icon.style.fill = this.bgColor;
  376.  
  377. const tooltip = document.querySelector(`#${CONFIG.classes.tooltipText}`);
  378. tooltip?.remove();
  379. });
  380. }
  381. }
  382.  
  383.  
  384. // Główna klasa aplikacji (z index.js)
  385. class TwitterBlocker {
  386. constructor() {
  387. this.isActionClicked = false;
  388. this.ownUsername = null;
  389. this.usernameToAction = null;
  390. this.blockerBgColor = document.querySelector("meta[name=theme-color]")?.content;
  391. this.blockerColor = CONFIG.colors[this.blockerBgColor];
  392.  
  393. this.settingsManager = new SettingsManager();
  394.  
  395. this.initMutationObserver();
  396. this.initCompleteListener();
  397. }
  398.  
  399. initMutationObserver() {
  400. const observer = new MutationObserver((mutations) => {
  401. mutations.forEach((mutation) => {
  402. if (mutation.addedNodes.length > 0) {
  403. this.handleDOMMutation(mutation);
  404. }
  405. });
  406. });
  407.  
  408. observer.observe(document.body, {
  409. childList: true,
  410. subtree: true,
  411. attributes: false,
  412. });
  413. }
  414.  
  415. handleDOMMutation(mutation) {
  416. this.setUsername();
  417. this.handleConfirmUserBlock(mutation);
  418. this.handleOptionsMenu(mutation);
  419. this.addBlockButtonsToTimelines(mutation);
  420. this.handleThankYouMessage(mutation);
  421. this.handleProfilePageBlockButton(mutation);
  422. this.handleEngagementPages(mutation);
  423. this.handleBlockCompletion(mutation);
  424. this.handleGrokButtonOverlap(mutation);
  425. this.addBlockButtonsToEngagementsPages(mutation);
  426. }
  427.  
  428. initCompleteListener() {
  429. GM_addValueChangeListener('oneClickBlockerCompletion', (name, old_value, new_value, remote) => {
  430. if (remote && new_value) {
  431. this.removeBlockedUser(new_value.username);
  432. }
  433. });
  434. }
  435.  
  436. setUsername() {
  437. if (!this.ownUsername) {
  438. this.ownUsername = document
  439. .querySelector(CONFIG.selectors.username)
  440. ?.querySelector("[tabindex]")?.textContent;
  441. }
  442. }
  443.  
  444. handleConfirmUserBlock(mutation) {
  445. const confirmButton = mutation.target.querySelector(CONFIG.selectors.confirmButton);
  446. if (!confirmButton?.classList.contains(CONFIG.classes.confirmationClicked)) {
  447. this.processActionConfirmation(mutation, confirmButton);
  448. }
  449. }
  450.  
  451. async processActionConfirmation(mutation, confirmButton) {
  452. if (!confirmButton) return;
  453.  
  454. confirmButton.classList.add(CONFIG.classes.confirmationClicked);
  455. const dialog = mutation.target.querySelector(CONFIG.selectors.confirmationDialog);
  456.  
  457. if (this.usernameToAction) {
  458. const containsUsername = dialog?.textContent?.toLowerCase()?.includes(this.usernameToAction.toLowerCase());
  459. if (containsUsername) {
  460. confirmButton.click();
  461. }
  462. }
  463.  
  464. if (document.querySelector(CONFIG.selectors.userActions)) {
  465. confirmButton.click();
  466. }
  467. }
  468.  
  469. handleOptionsMenu(mutation) {
  470. const menu = mutation.target.querySelector('[role="menu"]');
  471. if (menu && !menu.classList.contains(CONFIG.classes.menu)) {
  472. this.processMenuVisibility(menu);
  473. }
  474. }
  475.  
  476. async processMenuVisibility(menu) {
  477. menu.classList.add(CONFIG.classes.menu);
  478. if (this.isActionClicked) {
  479. const action = await this.getCurrentAction();
  480. const actionButton = action === 'mute' ?
  481. this.getMuteButton() :
  482. document.querySelector(CONFIG.selectors.blockButton);
  483.  
  484. menu.style.opacity = actionButton ? 0 : 1;
  485. if (!actionButton) menu.remove();
  486. } else {
  487. menu.style.opacity = 1;
  488. }
  489. }
  490.  
  491. getMuteButton() {
  492. const blockButton = document.querySelector(CONFIG.selectors.blockButton);
  493. if (!blockButton) return null;
  494.  
  495. const isStatusPage = location.pathname.includes('/status/');
  496.  
  497. const muteButton = isStatusPage ?
  498. blockButton.previousElementSibling?.previousElementSibling :
  499. blockButton.previousElementSibling;
  500.  
  501. if (muteButton?.textContent?.toLowerCase().includes('@')) {
  502. return muteButton;
  503. }
  504. return null;
  505. }
  506.  
  507. async triggerMenuOptionClick() {
  508. const action = await this.getCurrentAction();
  509. setTimeout(() => {
  510. const actionButton = action === 'mute' ?
  511. this.getMuteButton() :
  512. document.querySelector(CONFIG.selectors.blockButton);
  513.  
  514. this.isActionClicked = false;
  515. if (!actionButton) return;
  516. actionButton.click();
  517. }, 20);
  518. }
  519.  
  520. async sendActionRequest(username) {
  521. const action = await this.getCurrentAction();
  522. GM_openInTab(`https://x.com/${username}?${action}=true`, { active: false, setParent: true });
  523. }
  524.  
  525. async addBlockButtonsToTimelines(mutation) {
  526. if (!await this.shouldShowButtonOnPage('timeline')) return;
  527. const wrapper = this.detectTweetUsernameWrapper(mutation);
  528. if (wrapper) {
  529. const usernameDiv = wrapper.querySelector(CONFIG.selectors.userName);
  530. const menuButton = wrapper.querySelector(CONFIG.selectors.menuCaret);
  531. this.attachActionButton(usernameDiv, menuButton);
  532. }
  533. }
  534.  
  535. detectTweetUsernameWrapper(mutation) {
  536. let nodeOfInterest = null;
  537. const mutationString = Array.from(mutation.addedNodes)
  538. .map(node => node.outerHTML)
  539. .join("");
  540. const hasMenuCaret = mutationString.includes('data-testid="caret"');
  541.  
  542. if (hasMenuCaret) {
  543. mutation.addedNodes.forEach(node => {
  544. if (node.querySelector && node.querySelector('[data-testid="caret"]') && node.closest("article")) {
  545. let parent = node.parentElement;
  546. while (parent && !parent.querySelector('[data-testid="User-Name"]')) {
  547. if (parent.nodeName.toLowerCase() === "article") break;
  548. parent = parent.parentElement;
  549. }
  550. if (parent && parent.querySelector('[data-testid="User-Name"]')) {
  551. nodeOfInterest = parent;
  552. }
  553. }
  554. if (node.getAttribute && node.getAttribute("data-testid") === "cellInnerDiv") {
  555. nodeOfInterest = node;
  556. }
  557. });
  558. }
  559. return nodeOfInterest;
  560. }
  561.  
  562. handleThankYouMessage(mutation) {
  563. const mutationString = Array.from(mutation.addedNodes)
  564. .map(node => node.outerHTML)
  565. .join("");
  566.  
  567. if (mutationString.includes("X will use this to make your timeline better.")) {
  568. const wrapper = mutation.target.closest(CONFIG.selectors.cellInnerDiv);
  569. if (wrapper && wrapper.querySelectorAll('button').length < 3) {
  570. this.hideElement(wrapper);
  571. }
  572. }
  573. }
  574.  
  575. hideElement(element) {
  576. if (!element) return;
  577. element.style.height = "0px";
  578. element.style.visibility = "hidden";
  579. }
  580.  
  581. async handleProfilePageBlockButton(mutation) {
  582. const pageInfo = this.getProfilePageInfo();
  583. if (!pageInfo.isProfilePage || pageInfo.isOwnProfilePage) {
  584. return;
  585. }
  586.  
  587. const pageType = this.getPageType();
  588. if (!await this.shouldShowButtonOnPage(pageType)) {
  589. return;
  590. }
  591.  
  592. const userActionsButton = this.getUserActionsButton(mutation);
  593. if (!userActionsButton || this.hasExistingBlockButton(mutation, userActionsButton)) return;
  594.  
  595. this.addProfileBlockButton(userActionsButton);
  596. }
  597.  
  598. getProfilePageInfo() {
  599. const currentUsername = location.pathname.split("/")[1];
  600. let isProfilePage = () => {
  601. return !!document.querySelector(CONFIG.selectors.userActions);
  602. };
  603.  
  604. let isOwnProfilePage = () => {
  605. return (
  606. document
  607. .querySelector('[data-testid="SideNav_AccountSwitcher_Button"]')
  608. ?.querySelector("[tabindex]")
  609. ?.textContent?.split("@")?.[1] ==
  610. location.pathname.split("/")[location.pathname.split("/").length - 1]
  611. );
  612. };
  613.  
  614. return { isProfilePage: isProfilePage(), isOwnProfilePage: isOwnProfilePage() };
  615. }
  616.  
  617. getUserActionsButton(mutation) {
  618. return mutation.target.querySelector(CONFIG.selectors.userActions);
  619. }
  620.  
  621. hasExistingBlockButton(mutation, userActionsButton) {
  622. return (
  623. mutation.target.querySelector(`.${CONFIG.classes.blockButton}`) ||
  624. userActionsButton.parentElement.querySelector(`.${CONFIG.classes.blockButton}`)
  625. );
  626. }
  627.  
  628. addProfileBlockButton(userActionsButton) {
  629. const blockButton = new ProfileBlockButtonUI(this.blockerBgColor, this.blockerColor, this.settingsManager)
  630. .create();
  631. userActionsButton.parentElement.appendChild(blockButton);
  632. blockButton.classList.add(CONFIG.classes.blockButton);
  633.  
  634. this.addProfileBlockButtonListeners(blockButton, userActionsButton);
  635. this.handleAutoBlock(blockButton);
  636. }
  637.  
  638. addProfileBlockButtonListeners(blockButton, userActionsButton) {
  639. blockButton.addEventListener("click", () => {
  640. this.usernameToAction = location.pathname.split("/")[1];
  641. userActionsButton.click();
  642. this.triggerMenuOptionClick();
  643. });
  644. }
  645.  
  646. handleAutoBlock(blockButton) {
  647. const urlParams = new URLSearchParams(window.location.search);
  648. if (urlParams.get("block") || urlParams.get("mute")) {
  649. if (this.checkForError()) {
  650. this.showError("Something went wrong with the action.");
  651. setTimeout(() => window.close(), 3000);
  652. return;
  653. }
  654. blockButton.click();
  655. }
  656. }
  657.  
  658. checkForError() {
  659. return document
  660. .querySelector(CONFIG.selectors.primaryColumn)
  661. ?.textContent.includes("Something went wrong");
  662. }
  663.  
  664. handleGrokButtonOverlap(mutation) {
  665. const grokButton = mutation.target.querySelector(CONFIG.selectors.grokActions);
  666. if (grokButton) {
  667. const blockButton = grokButton.closest("article")
  668. .querySelector(`.${CONFIG.classes.blockButtonWrapper}`);
  669. if (blockButton) {
  670. blockButton.style.right = "45px";
  671. }
  672. }
  673. }
  674.  
  675. removeBlockedUser(username) {
  676. const userCells = document.querySelectorAll(`[href="/${username}"]`);
  677. userCells.forEach(cell => {
  678. const userCellParent = cell.closest(CONFIG.selectors.userCell);
  679. userCellParent?.remove();
  680.  
  681. const tweetArticle = cell.closest('article');
  682. tweetArticle?.remove();
  683. });
  684. }
  685.  
  686. showError(message) {
  687. let errorDiv = document.getElementById('one-click-blocker-error');
  688. if (!errorDiv) {
  689. errorDiv = document.createElement("div");
  690. errorDiv.id = 'one-click-blocker-error';
  691. Object.assign(errorDiv.style, CONFIG.styles.errorWrapper);
  692. document.body.appendChild(errorDiv);
  693. }
  694. errorDiv.textContent = message;
  695. setTimeout(() => errorDiv.remove(), 5000);
  696. }
  697.  
  698. async getCurrentAction() {
  699. const settings = await this.settingsManager.loadSettings();
  700. return settings.action || 'block';
  701. }
  702.  
  703. getPageType() {
  704. const path = window.location.pathname;
  705.  
  706. if (path.includes('/search')) return 'search';
  707. if (path.includes('/retweets') || path.includes('/likes') || path.includes('/quotes')) return 'engagements';
  708. if (path.includes('/followers') || path.includes('/verified_followers')) return 'followers';
  709. if (path.includes('/hashtag')) return 'hashtag';
  710. if (path.includes('/i/trending')) return 'trending';
  711. if (path.includes('/notifications')) return 'notifications';
  712. if (path.includes('/following')) return 'following';
  713. if (path.includes('/i/communities')) return 'communities';
  714. if (path === '/home' || path === '/') return 'timeline';
  715. if (path.match(/\/status\/\d+/)) return 'status';
  716. if (document.querySelector(CONFIG.selectors.userActions)) return 'profile';
  717. if (path.includes('/explore')) return 'explore';
  718. return null;
  719. }
  720.  
  721. async shouldShowButtonOnPage(pageType) {
  722. const settings = await this.settingsManager.loadSettings();
  723. const currentPageType = this.getPageType();
  724.  
  725. if (currentPageType) {
  726. return settings.enabledPages?.[currentPageType] ?? true;
  727. }
  728. return settings.enabledPages?.[pageType] ?? true;
  729. }
  730.  
  731. async attachActionButton(userNameDiv, menuOptionsButton) {
  732. if (!userNameDiv) return;
  733. if (menuOptionsButton.parentElement.querySelector(`.${CONFIG.classes.blockButtonWrapper}`)) return;
  734.  
  735. const tweetUsername = userNameDiv.querySelectorAll("[href]")?.[1]?.textContent;
  736. if (!this.shouldAddActionButton(tweetUsername)) return;
  737.  
  738. const action = await this.getCurrentAction();
  739. const blockButton = new BlockButtonUI(this.blockerBgColor, this.blockerColor)
  740. .create(tweetUsername, menuOptionsButton, action);
  741.  
  742. this.addTweetActionButtonListeners(blockButton, menuOptionsButton);
  743. const menuOptionsWrapper = menuOptionsButton.parentElement;
  744. menuOptionsWrapper.insertBefore(blockButton, menuOptionsButton);
  745. }
  746.  
  747. shouldAddActionButton(tweetUsername) {
  748. return tweetUsername !== this.ownUsername;
  749. }
  750.  
  751. addTweetActionButtonListeners(actionButton, menuOptionsButton) {
  752. actionButton.addEventListener("click", async (e) => {
  753. this.usernameToAction = actionButton.dataset.username;
  754. e.stopPropagation();
  755. this.triggerMenuClick(menuOptionsButton);
  756. // this.hideTweetDiv(menuOptionsButton);
  757. await this.triggerMenuOptionClick();
  758. });
  759. }
  760.  
  761. triggerMenuClick(menuOptionsButton) {
  762. this.isActionClicked = true;
  763. menuOptionsButton.click();
  764. }
  765.  
  766. async handleEngagementPages(mutation) {
  767. if (!await this.shouldShowButtonOnPage('engagements')) return;
  768.  
  769. const userCell = mutation.target.querySelector(CONFIG.selectors.userCell);
  770. if (!userCell) return;
  771.  
  772. const userActions = userCell.querySelector(CONFIG.selectors.userActions);
  773. if (!userActions || userActions.parentElement.querySelector(`.${CONFIG.classes.blockButton}`)) return;
  774.  
  775. const blockButton = new ProfileBlockButtonUI(this.blockerBgColor, this.blockerColor, this.settingsManager)
  776. .create('engagements-page');
  777. userActions.parentElement.appendChild(blockButton);
  778. blockButton.classList.add(CONFIG.classes.blockButton);
  779.  
  780. blockButton.addEventListener('click', () => {
  781. const username = userCell.querySelector('a[href^="/"]')?.getAttribute('href')?.substring(1);
  782. if (username) {
  783. this.usernameToAction = username;
  784. userActions.click();
  785. this.triggerMenuOptionClick();
  786. }
  787. });
  788. }
  789.  
  790. handleBlockCompletion(mutation) {
  791. const mutationString = Array.from(mutation.addedNodes)
  792. .map(node => node.outerHTML)
  793. .join("");
  794.  
  795. const isBlocked = mutationString.includes("blocked");
  796. const isMuted = mutationString.includes("muted");
  797.  
  798. if (isBlocked || isMuted) {
  799. let username = this.usernameToAction;
  800. const urlParams = new URLSearchParams(window.location.search);
  801. const isFromUrl = urlParams.get("block") || urlParams.get("mute");
  802.  
  803. if (!username && isFromUrl) {
  804. username = location.pathname.split("/")[1];
  805. }
  806.  
  807. if (username) {
  808. username = username.replace("@", "");
  809. this.saveUserToStorage(username, isBlocked ? 'block' : 'mute');
  810.  
  811. this.usernameToAction = null;
  812.  
  813. if (isFromUrl) {
  814. GM_setValue('oneClickBlockerCompletion', {
  815. username: username,
  816. action: isBlocked ? 'block' : 'mute',
  817. timestamp: Date.now()
  818. });
  819. setTimeout(() => window.close(), 500);
  820. }
  821. }
  822. }
  823. }
  824.  
  825. async saveUserToStorage(username, action) {
  826. try {
  827. const settings = await this.settingsManager.loadSettings();
  828. if (!settings.stats) settings.stats = { ...DEFAULT_SETTINGS.stats };
  829. if (!settings.stats.blockedUsers) settings.stats.blockedUsers = [];
  830. if (!settings.stats.mutedUsers) settings.stats.mutedUsers = [];
  831.  
  832. if (action === 'block') {
  833. if (!settings.stats.blockedUsers.includes(username)) {
  834. settings.stats.blockedUsers.push(username);
  835. settings.stats.totalBlocks = (settings.stats.totalBlocks || 0) + 1;
  836. }
  837. } else if (action === 'mute') {
  838. if (!settings.stats.mutedUsers.includes(username)) {
  839. settings.stats.mutedUsers.push(username);
  840. settings.stats.totalMutes = (settings.stats.totalMutes || 0) + 1;
  841. }
  842. }
  843. await this.settingsManager.saveSettings(settings);
  844. } catch (error) {
  845. console.error('Error saving user to storage:', error);
  846. }
  847. }
  848.  
  849. async addBlockButtonsToEngagementsPages(mutation) {
  850. let isAUserProfilePage = document.querySelector('[data-testid="userActions"]');
  851. const pageType = this.getPageType();
  852. if (!await this.shouldShowButtonOnPage(pageType) || isAUserProfilePage) return;
  853.  
  854. if (!mutation.target.querySelector || !mutation.target.querySelector('[data-testid="cellInnerDiv"]')?.textContent) {
  855. return;
  856. }
  857.  
  858. const followButtons = mutation.target.querySelectorAll(CONFIG.selectors.followButton);
  859. followButtons.forEach((followButton) => {
  860. if (followButton.closest(CONFIG.selectors.hoverCard) || followButton.parentElement.querySelector(".sw-block-button")) return;
  861.  
  862. const followButtonParentTweet = followButton.closest(CONFIG.selectors.tweet);
  863. if (followButtonParentTweet) return;
  864.  
  865. const blockButton = new ProfileBlockButtonUI(this.blockerBgColor, this.blockerColor, this.settingsManager)
  866. .create("engagements-page");
  867. followButton.parentElement.appendChild(blockButton);
  868. followButton.parentElement.style.flexDirection = "row";
  869. blockButton.classList.add("sw-block-button");
  870.  
  871. blockButton.addEventListener("click", (e) => {
  872. e.preventDefault();
  873. e.stopPropagation();
  874.  
  875. const username = followButton?.closest('[data-testid="UserCell"]')?.querySelector('a[href^="/"]')?.getAttribute('href')?.substring(1);
  876. if (username) {
  877. this.usernameToAction = username;
  878. this.sendActionRequest(username);
  879. this.removeBlockedUser(username);
  880. }
  881. });
  882. });
  883. }
  884. }
  885.  
  886. // --- Panel Ustawień ---
  887.  
  888. const settingsPanelCSS = `
  889. :root { --twitter-blue: rgb(29, 155, 240); --twitter-background: rgb(239, 243, 244); --twitter-border: rgb(207, 217, 222); --twitter-text: rgb(15, 20, 25); --twitter-hover: rgba(15, 20, 25, 0.1); }
  890. #ocb-settings-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 680px; max-height: 90vh; background-color: white; border-radius: 16px; z-index: 10000; display: flex; flex-direction: column; box-shadow: rgba(101, 119, 134, 0.2) 0px 0px 15px, rgba(101, 119, 134, 0.15) 0px 0px 3px 1px; }
  891. #ocb-settings-overlay { position: fixed; inset: 0px; background-color: rgba(91, 112, 131, 0.4); z-index: 9999; }
  892. #ocb-settings-panel .header { display: flex; align-items: center; padding: 16px; border-bottom: 1px solid var(--twitter-border); }
  893. #ocb-settings-panel h1 { font-size: 20px; font-weight: bold; margin: 0; flex-grow: 1; }
  894. #ocb-settings-panel .close-btn { cursor: pointer; font-size: 24px; padding: 0 8px; }
  895. #ocb-settings-panel .nav { display: flex; gap: 8px; padding: 0 16px; border-bottom: 1px solid var(--twitter-border); flex-shrink: 0; overflow-x: auto; }
  896. #ocb-settings-panel .nav-item { padding: 12px 16px; cursor: pointer; font-weight: bold; transition: background-color 0.2s; white-space: nowrap; border-bottom: 2px solid transparent;}
  897. #ocb-settings-panel .nav-item:hover { background-color: var(--twitter-hover); }
  898. #ocb-settings-panel .nav-item.active { color: var(--twitter-blue); border-bottom-color: var(--twitter-blue); }
  899. #ocb-settings-panel .container { padding: 20px; overflow-y: auto; }
  900. #ocb-settings-panel .section { padding-bottom: 20px; border-bottom: 1px solid var(--twitter-border); margin-bottom: 20px; }
  901. #ocb-settings-panel .section-title { font-size: 18px; font-weight: bold; margin-bottom: 16px; }
  902. #ocb-settings-panel .option-group { display: flex; flex-direction: column; gap: 12px; }
  903. #ocb-settings-panel .option { display: flex; align-items: center; }
  904. #ocb-settings-panel .option label { cursor: pointer; padding: 8px; flex-grow: 1; }
  905. #ocb-settings-panel .button { background-color: var(--twitter-blue); color: white; border: none; padding: 12px 24px; border-radius: 20px; font-weight: bold; cursor: pointer; }
  906. #ocb-settings-panel .button:hover { background-color: rgb(26, 140, 216); }
  907. #ocb-settings-panel .button.danger { background-color: rgb(231, 76, 60); }
  908. #ocb-settings-panel .button.danger:hover { background-color: rgb(192, 57, 43); }
  909. #ocb-settings-panel .tab-content { display: none; }
  910. #ocb-settings-panel .tab-content.active { display: block; }
  911. #ocb-settings-panel .blocked-user { display: block; background-color: var(--twitter-background); border-radius: 8px; margin-bottom: 8px; }
  912. #ocb-settings-panel .user-link { color: var(--twitter-text); text-decoration: none; display: block; padding: 16px; }
  913. #ocb-settings-panel .user-link:hover { color: var(--twitter-blue); }
  914. #ocb-settings-panel .save-notice { text-align: center; color: var(--twitter-blue); padding-top: 10px; opacity: 0; transition: opacity 0.3s; }
  915. #ocb-settings-panel .save-notice.visible { opacity: 1; }
  916. #ocb-settings-panel .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
  917. #ocb-settings-panel .stat-card { background-color: var(--twitter-background); padding: 16px; border-radius: 16px; text-align: center; }
  918. #ocb-settings-panel .stat-number { font-size: 24px; font-weight: bold; }
  919. #ocb-settings-panel .stat-label { color: rgb(83, 100, 113); }
  920. #ocb-settings-panel .button-group { display: flex; gap: 8px; flex-wrap: wrap; }
  921. `;
  922.  
  923. const settingsPanelHTML = `
  924. <div id="ocb-settings-overlay"></div>
  925. <div id="ocb-settings-panel">
  926. <div class="header">
  927. <h1>One-Click Blocker Settings</h1>
  928. <span class="close-btn">&times;</span>
  929. </div>
  930. <div class="nav">
  931. <div class="nav-item active" data-tab="settings">Settings</div>
  932. <div class="nav-item" data-tab="blocked">Blocked</div>
  933. <div class="nav-item" data-tab="muted">Muted</div>
  934. <div class="nav-item" data-tab="stats">Statistics</div>
  935. <div class="nav-item" data-tab="backup">Backup</div>
  936. </div>
  937. <div class="container">
  938. <div id="settings" class="tab-content active">
  939. <!-- Zawartość z options.html -->
  940. </div>
  941. <div id="blocked" class="tab-content"></div>
  942. <div id="muted" class="tab-content"></div>
  943. <div id="stats" class="tab-content"></div>
  944. <div id="backup" class="tab-content"></div>
  945. <div class="save-notice">Settings saved!</div>
  946. </div>
  947. </div>
  948. `;
  949.  
  950. class OptionsManager {
  951. constructor(settingsManager) {
  952. this.settingsManager = settingsManager;
  953. this.createPanel();
  954. this.loadSettings();
  955. this.initializeUI();
  956. }
  957.  
  958. createPanel() {
  959. GM_addStyle(settingsPanelCSS);
  960. const panelContainer = document.createElement('div');
  961. panelContainer.innerHTML = settingsPanelHTML;
  962. document.body.appendChild(panelContainer);
  963. this.panel = document.getElementById('ocb-settings-panel');
  964. this.overlay = document.getElementById('ocb-settings-overlay');
  965. }
  966.  
  967. async loadSettings() {
  968. const settings = await this.settingsManager.loadSettings();
  969. this.updateUI(settings);
  970. }
  971.  
  972. async saveSettings() {
  973. const currentSettings = await this.settingsManager.loadSettings();
  974. const settings = {
  975. action: document.querySelector('#ocb-settings-panel input[name="action"]:checked').value,
  976. enabledPages: Object.fromEntries(
  977. Array.from(document.querySelectorAll('#ocb-settings-panel input[name="pages"]'))
  978. .map(checkbox => [checkbox.value, checkbox.checked])
  979. ),
  980. showSettingsIcon: document.getElementById('show-settings-icon').checked,
  981. stats: currentSettings.stats
  982. };
  983. await this.settingsManager.saveSettings(settings);
  984. this.showSaveNotice();
  985. }
  986.  
  987. updateUI(settings) {
  988. this.panel.querySelector('#settings').innerHTML = `
  989. <div class="section">
  990. <div class="section-title">Default Action</div>
  991. <div class="option-group">
  992. <div class="option"><label><input type="radio" name="action" value="block" ${settings.action === 'block' ? 'checked' : ''}> Block</label></div>
  993. <div class="option"><label><input type="radio" name="action" value="mute" ${settings.action === 'mute' ? 'checked' : ''}> Mute</label></div>
  994. </div>
  995. </div>
  996. <div class="section">
  997. <div class="section-title">Interface Settings</div>
  998. <div class="option-group"><div class="option"><label><input type="checkbox" id="show-settings-icon" ${settings.showSettingsIcon ? 'checked' : ''}> Show settings icon</label></div></div>
  999. </div>
  1000. <div class="section">
  1001. <div class="section-title">Show Button On</div>
  1002. <div class="option-group">
  1003. ${Object.entries(DEFAULT_SETTINGS.enabledPages).map(([key, value]) => `
  1004. <div class="option"><label><input type="checkbox" name="pages" value="${key}" ${settings.enabledPages[key] ? 'checked' : ''}> ${key.charAt(0).toUpperCase() + key.slice(1)}</label></div>
  1005. `).join('')}
  1006. </div>
  1007. </div>
  1008. `;
  1009. this.panel.querySelector('#blocked').innerHTML = `<div class="section"><div class="section-title">Blocked Users</div><div id="blocked-users-list">${this.renderUserList(settings.stats.blockedUsers)}</div></div>`;
  1010. this.panel.querySelector('#muted').innerHTML = `<div class="section"><div class="section-title">Muted Users</div><div id="muted-users-list">${this.renderUserList(settings.stats.mutedUsers)}</div></div>`;
  1011. this.panel.querySelector('#stats').innerHTML = `<div class="section"><div class="section-title">Action Statistics</div><div class="stats"><div class="stat-card"><div class="stat-number">${settings.stats.totalBlocks}</div><div class="stat-label">Total Blocks</div></div><div class="stat-card"><div class="stat-number">${settings.stats.totalMutes}</div><div class="stat-label">Total Mutes</div></div></div></div>`;
  1012. this.panel.querySelector('#backup').innerHTML = `<div class="section"><div class="section-title">Backup & Restore</div><div class="button-group"><button class="button" id="export-data">Export</button><button class="button" id="import-data">Import</button><button class="button danger" id="clear-data">Reset</button></div></div>`;
  1013. }
  1014.  
  1015. renderUserList(users) {
  1016. return users.length ? users.map(user => `<div class="blocked-user"><a href="https://x.com/${user}" target="_blank" class="user-link">@${user}</a></div>`).join('') : '<p>No users yet.</p>';
  1017. }
  1018.  
  1019. showSaveNotice() {
  1020. const notice = this.panel.querySelector('.save-notice');
  1021. notice.classList.add('visible');
  1022. setTimeout(() => notice.classList.remove('visible'), 2000);
  1023. }
  1024.  
  1025. initializeUI() {
  1026. this.panel.addEventListener('change', e => {
  1027. if (e.target.matches('input[type="radio"], input[type="checkbox"]')) {
  1028. this.saveSettings();
  1029. }
  1030. });
  1031.  
  1032. this.panel.querySelector('.close-btn').addEventListener('click', () => this.hide());
  1033. this.overlay.addEventListener('click', () => this.hide());
  1034.  
  1035. this.panel.querySelectorAll('.nav-item').forEach(item => {
  1036. item.addEventListener('click', () => {
  1037. this.panel.querySelector('.nav-item.active').classList.remove('active');
  1038. item.classList.add('active');
  1039. this.panel.querySelector('.tab-content.active').classList.remove('active');
  1040. this.panel.querySelector(`#${item.dataset.tab}`).classList.add('active');
  1041. });
  1042. });
  1043.  
  1044. this.panel.querySelector('#export-data').addEventListener('click', () => this.exportData());
  1045. this.panel.querySelector('#import-data').addEventListener('click', () => this.importData());
  1046. this.panel.querySelector('#clear-data').addEventListener('click', () => this.clearData());
  1047. }
  1048.  
  1049. async exportData() {
  1050. const settings = await this.settingsManager.loadSettings();
  1051. const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
  1052. const url = URL.createObjectURL(blob);
  1053. const a = document.createElement('a');
  1054. a.href = url;
  1055. a.download = `one-click-blocker-backup.json`;
  1056. a.click();
  1057. URL.revokeObjectURL(url);
  1058. }
  1059.  
  1060. importData() {
  1061. const input = document.createElement('input');
  1062. input.type = 'file';
  1063. input.accept = 'application/json';
  1064. input.onchange = async (e) => {
  1065. const file = e.target.files[0];
  1066. if (!file) return;
  1067. const text = await file.text();
  1068. const importedData = JSON.parse(text);
  1069. await this.settingsManager.saveSettings(importedData);
  1070. this.updateUI(importedData);
  1071. alert('Settings imported successfully!');
  1072. };
  1073. input.click();
  1074. }
  1075.  
  1076. async clearData() {
  1077. if (confirm('Are you sure you want to reset all settings and data? This cannot be undone.')) {
  1078. await this.settingsManager.saveSettings(DEFAULT_SETTINGS);
  1079. this.updateUI(DEFAULT_SETTINGS);
  1080. alert('Settings have been reset.');
  1081. }
  1082. }
  1083.  
  1084. show() { this.panel.style.display = 'flex'; this.overlay.style.display = 'block'; }
  1085. hide() { this.panel.style.display = 'none'; this.overlay.style.display = 'none'; }
  1086. }
  1087.  
  1088. // --- Inicjalizacja ---
  1089. const twitterBlocker = new TwitterBlocker();
  1090. let optionsManagerInstance = null;
  1091.  
  1092. function showSettingsPanel() {
  1093. if (!optionsManagerInstance) {
  1094. optionsManagerInstance = new OptionsManager(twitterBlocker.settingsManager);
  1095. }
  1096. optionsManagerInstance.show();
  1097. }
  1098.  
  1099. GM_registerMenuCommand('1-Click Blocker Settings', showSettingsPanel);
  1100.  
  1101. })();
Advertisement
Add Comment
Please, Sign In to add comment