V2EX Ignore

在 V2EX 的每个帖子标题旁添加一个“忽略”按钮,单击即可忽略该主题。

  1. // ==UserScript==
  2. // @name V2EX Ignore
  3. // @license GNU GPLv3
  4. // @namespace https://www.example.com/
  5. // @version 2.0
  6. // @description Adds an "Ignore" button next to each post's title on V2EX that allows you to ignore the topic with a single click.
  7. // @description:zh-CN 在 V2EX 的每个帖子标题旁添加一个“忽略”按钮,单击即可忽略该主题。
  8. // @description:zh-TW 在 V2EX 的每個帖子標題旁添加一個“忽略”按鈕,單擊即可忽略該主題。
  9. // @description:ja 在 V2EX の各投稿タイトルの横に、「無視する」ボタンを追加し、クリックするだけでそのトピックを無視できます。
  10. // @author Arryboom
  11. // @match https://*.v2ex.com/*
  12. // @match https://v2ex.com/*
  13. // @grant GM_xmlhttpRequest
  14. // @run-at document-end
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // Function to ignore a topic
  21. function ignoreTopic(event) {
  22. event.preventDefault();
  23. const topicLink = event.target.parentNode.querySelector('.item_title a');
  24. if (topicLink) {
  25. const topicUrl = topicLink.href;
  26. const topicId = getTopicIdFromUrl(topicUrl);
  27. if (topicId) {
  28. getOnce().then(onceValue => {
  29. const ignoreUrl = `https://${window.location.hostname}/ignore/topic/${topicId}?once=${onceValue}`;
  30. GM_xmlhttpRequest({
  31. method: 'GET',
  32. url: ignoreUrl,
  33. onload: function(response) {
  34. const post = event.target.closest('.cell.item');
  35. if (post) {
  36. post.remove();
  37. showToast(`Topic ${topicId} ignored.`);
  38. }
  39. },
  40. onerror: function(error) {
  41. console.error(`Error ignoring topic ${topicId}: ${error}`);
  42. }
  43. });
  44. }).catch(error => {
  45. console.error(`Error fetching once value: ${error}`);
  46. });
  47. }
  48. }
  49. }
  50.  
  51. // Function to extract the topic ID from a topic URL
  52. function getTopicIdFromUrl(topicUrl) {
  53. const match = topicUrl.match(/\/t\/(\d+)/);
  54. if (match) {
  55. return match[1];
  56. }
  57. return null;
  58. }
  59.  
  60. // Function to fetch the current once value from the V2EX server
  61. function getOnce() {
  62. return CSRF.getOnceCode()
  63. .then(code => {
  64. return code;
  65. })
  66. .catch(err => {
  67. console.error(err);
  68. throw err;
  69. });
  70. }
  71.  
  72. class CSRF {
  73. static getOnceCode() {
  74. return new Promise((resolve, reject) => {
  75. fetch('/settings/block')
  76. .then(resp => resp.text())
  77. .then(text => {
  78. const code = CSRF.parseOnceCode(text);
  79. resolve(code);
  80. })
  81. .catch(reject);
  82. });
  83. }
  84.  
  85. static parseOnceCode(text) {
  86. return text.match(/once=(\d+)/)[1];
  87. }
  88. }
  89.  
  90. // Function to display a toast message
  91. function showToast(message) {
  92. const toast = document.createElement('div');
  93. toast.textContent = message;
  94. toast.style.position = 'fixed';
  95. toast.style.bottom = '20px';
  96. toast.style.right = '20px';
  97. toast.style.padding = '10px';
  98. toast.style.backgroundColor = '#333';
  99. toast.style.color = '#fff';
  100. toast.style.borderRadius = '5px';
  101. document.body.appendChild(toast);
  102. setTimeout(() => {
  103. toast.remove();
  104. }, 3000);
  105. }
  106.  
  107. // Find all post titles on the page and add an "Ignore" button next to each one
  108. const postTitles = document.querySelectorAll('.cell.item .item_title');
  109. postTitles.forEach(postTitle => {
  110. const ignoreButton = document.createElement('a');
  111. ignoreButton.textContent = 'Ignore';
  112. ignoreButton.href = '#';
  113. ignoreButton.classList.add('tag');
  114. ignoreButton.addEventListener('click', ignoreTopic);
  115. postTitle.parentNode.insertBefore(ignoreButton, postTitle.nextSibling);
  116. });
  117. })();