Lemmy post utilities - Filter posts by title

Filters posts on any lemmy instance by text in the title. It can also auto-open image posts, unblur thumbnails, and other things.

目前为 2023-08-30 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Lemmy post utilities - Filter posts by title
  3. // @namespace Violentmonkey Scripts
  4. // @description Filters posts on any lemmy instance by text in the title. It can also auto-open image posts, unblur thumbnails, and other things.
  5. // @match https://*lemmy*.*/*
  6. // @include https://lemy.nl/*
  7. // @include https://burggit.moe/*
  8. // @include https://lemmit.online/*
  9. // @include https://yiffit.net/*
  10. // @include https://reddthat.com/*
  11. // @include https://sh.itjust.works/*
  12. // @exclude https://lemmyverse.net/*
  13. // @exclude https://lemmy-status.org/*
  14. // @exclude https://search-lemmy.com/*
  15. // @exclude https://join-lemmy.org/*
  16. // @grant GM_getValue
  17. // @grant GM_setValue
  18. // @grant GM_setClipboard
  19. // @version 2.2.3
  20. // @icon data:image/png;base64,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
  21. // @author Xynoth
  22. // @license GPT-3
  23. // @date 14/7/2023, 20:15:03
  24. // ==/UserScript==
  25.  
  26. //----------------------------------------
  27. // CONSTANTS
  28. //----------------------------------------
  29.  
  30. // Key constants
  31. const BLOCKED_TAGS_KEY = "blockedTags";
  32. const BLOCKED_CASE_SENSITIVE_TAGS_KEY = "blockedCaseSensitiveTags";
  33. const AUTO_OPEN_MEDIA_POSTS_KEY = "autoOpenMediaPosts";
  34. const UNBLUR_THUMBNAILS_KEY = "unblurThumbs";
  35. const SHOW_FILTERED_STUBS_KEY = "showFilteredStubs";
  36. const COMMAS_AS_SEPARATORS_KEY = "useCommasAsSeparators";
  37. const CASE_SENSITIVE_KEY = "caseSensitiveTag";
  38. const WIDGET_HEIGHT_KEY = "widgetHeight";
  39. const FIX_BROKEN_VIDEO_PREVIEWS_KEY = "fixBrokenVideoPreviews";
  40. const MARK_POSTS_AS_NSFW_KEY = "markNewPostsAsNSFW";
  41.  
  42. // Constant selectors
  43. const postsContainerClass = "post-listings";
  44. const profileContainerClass = "person-details";
  45. const searchContainerClass = "search"
  46. const postContainer = "post-listing";
  47. const loadingSpinnerSelector = ".icon.spin"; // This is the "creators" select in the search page
  48. const postCommunityContainer = ".community-link";
  49. const postCommunityNameContainer = `${postCommunityContainer} > span`;
  50. const postSrcLink = `div:nth-of-type(2) p a`;
  51. const fixedPreviewContainerClass = "fixed-preview";
  52. const fixedPreviewVideoClass = "fixed-preview-video";
  53. const postAsNSFW = "#post-nsfw[type='checkbox']";
  54. const createPostContainerId = "createPostForm";
  55. const editPostClass = "post-form";
  56.  
  57. // GUI main elements
  58. const settingsWidgetId = "settings-widget";
  59. const blockedTagsDialogId = "tags-dialog";
  60. const dialogBgId = "dialog-bg";
  61. const settingsWidgetContainerId = "widget-container";
  62. const blockTagListId = "blocked-insensitive-tag-list";
  63. const csBlockTagListId = "blocked-sensitive-tags-list";
  64. const openedPostChecker = "already-opened";
  65. const filteredPostChecker = "filter-checked";
  66. const processedPageChecker = "processed-page";
  67.  
  68. // CSS Color constants
  69. const caseInsensitiveTagColor = "#dd2222";
  70. const caseSensitiveTagColor = "#2052b3";
  71. const primaryBtnColor = "#0052cc";
  72. const primaryBtnHoverColor = "#0066ff";
  73. const primaryBtnActiveColor = "#0047b3";
  74. const widgetBgColor = "#1a1a1b";
  75. const errorToastColor = "#f94b4b";
  76. const errorToastBgColor = "#2f0808";
  77.  
  78. // Other constants
  79. const logoImg = GM_info.script.icon;
  80. const namedRegex = "regex\\((.*?)\\):";
  81.  
  82. // HTML content constants
  83. const bottomWidgetContent = `
  84. <div id="${settingsWidgetContainerId}">
  85. <h1><span id="widget-logo"></span>Lemmy post utilities</h1>
  86. <div class="form-entry">
  87. <label>Blocked tags: </label>
  88. <div class="btn-container">
  89. <button id="block-tag-btn" type="button">🖊</button>
  90. </div>
  91. </div>
  92. <div class="form-entry">
  93. <label>
  94. Show stubs on filter:
  95. </label>
  96. <div class="btn-container">
  97. <span class="switch" id="show-stub-swt">
  98. <input type="checkbox">
  99. <span class="slider round"></span>
  100. </span>
  101. </div>
  102. </div>
  103. <div class="form-entry">
  104. <label>
  105. Fix broken video previews:
  106. </label>
  107. <div class="btn-container">
  108. <span class="switch" id="fix-video-previews-swt">
  109. <input type="checkbox">
  110. <span class="slider round"></span>
  111. </span>
  112. </div>
  113. </div>
  114. <div class="form-entry">
  115. <label>
  116. Auto open media previews:
  117. </label>
  118. <div class="btn-container">
  119. <span class="switch" id="auto-open-swt">
  120. <input type="checkbox">
  121. <span class="slider round"></span>
  122. </span>
  123. </div>
  124. </div>
  125. <div class="form-entry">
  126. <label>
  127. Unblur NSFW thumbnails:
  128. </label>
  129. <div class="btn-container">
  130. <span class="switch" id="unblur-swt">
  131. <input type="checkbox">
  132. <span class="slider round"></span>
  133. </span>
  134. </div>
  135. </div>
  136. <div class="form-entry">
  137. <label>
  138. Mark new posts as NSFW:
  139. </label>
  140. <div class="btn-container">
  141. <span class="switch" id="default-nsfw-posts-swt">
  142. <input type="checkbox">
  143. <span class="slider round"></span>
  144. </span>
  145. </div>
  146. </div>
  147. </div>
  148. `;
  149.  
  150. const tagsDialogContent = `
  151. <div id="blocked-tags-dialog-container">
  152. <div id ="blocked-tags-dialog-head">
  153. <h1>Blocked tags editor</h1>
  154. <button type="button" class="close-dialog-btn">⨯</button>
  155. </div>
  156. <div>
  157. <p>Any tag added here will hide any post that contains the word in its title.</p>
  158. <p>You can also use some advanced filtering options like the following:</p>
  159. <ul>
  160. <li>Use <a href="https://regex101.com/">regex</a> starting a tag with <code>regex:</code></li>
  161. <li>Create a named regex starting a tag with <code>regex(your-regex-name):</code></li>
  162. <li>Filter by linked source instead of title starting a tag with <code>source:</code></li>
  163. <li>Combine <code>source:</code> with any variant of <code>regex:</code></li>
  164. <li>You can click any tag to copy it's raw value.</li>
  165. </ul>
  166. <p>These are the blocked tags you have for this instance:</p>
  167. <div id="blocked-tags-field-container">
  168. <div id="blocked-tags-field">
  169. <p id="empty-blocked-tags">You haven't blocked anything yet.</p>
  170. <ul id="${blockTagListId}" class="blocked-tags-list" hidden>
  171. </ul>
  172. <hr id="blocked-tags-separator" hidden>
  173. <ul id="${csBlockTagListId}" class="blocked-tags-list" hidden>
  174. </ul>
  175. </div>
  176. <div id="blocked-tags-field-legend">
  177. <span id="tag-case-insensitive-legend" class="tag-legend">
  178. <span class="tag-color-legend"></span>
  179. <small class="tag-label-legend">Case insensitive tag</small>
  180. </span>
  181. <span id="tag-case-sensitive-legend" class="tag-legend">
  182. <span class="tag-color-legend"></span>
  183. <small class="tag-label-legend">Case sensitive tag</small>
  184. </span>
  185. </div>
  186. </div>
  187. <p id="clipboard-notice" hidden>Tag value copied to the clipboard!</p>
  188. <div class="switch-container">
  189. <label>
  190. Use commas as tag separators:
  191. </label>
  192. <div class="btn-container">
  193. <span class="switch" id="use-commas-swt">
  194. <input type="checkbox">
  195. <span class="slider round"></span>
  196. </span>
  197. </div>
  198. </div>
  199. <div class="switch-container">
  200. <label>
  201. Add tags as case sensitive:
  202. </label>
  203. <div class="btn-container">
  204. <span class="switch" id="case-sensitive-swt">
  205. <input type="checkbox">
  206. <span class="slider round"></span>
  207. </span>
  208. </div>
  209. </div>
  210. <div id="tag-input-container">
  211. <input type="text" id="tag-input" placeholder="Add your tags">
  212. <button type="button" id="tag-save-btn">Save</button>
  213. </div>
  214. <label id="blocked-tags-input-error" hidden>Tag must be longer than 1 non-whitespace character!</label>
  215. </div>
  216. </div>
  217. `;
  218.  
  219. const tagContent = `
  220. <span title="TOOLTIP-CONTENT">
  221. <label>TAG-NAME</label>
  222. <button type="button">⨯</button>
  223. </span>
  224. `;
  225.  
  226. const filteredPostStubContent = `
  227. <div class="hidden-post-stub-meta-container">
  228. <p>This post was hidden because it contained the tag 'TAG'.</p>
  229. <span class="hidden-post-stub-btn-container">
  230. <button class="show-hidden-post-title-btn" type="button">Show title</button>
  231. <button class="show-hidden-post-btn" type="button">Show post</button>
  232. </span>
  233. </div>
  234. <span class="stub-hidden-post-title" hidden>
  235. <br>
  236. <p>Post title was '<b>POST-TITLE</b>' from <a class="stub-hidden-post-community-link">POST-COMMUNITY</a> community.</p>
  237. </span>
  238. `;
  239.  
  240. const videoSourceContent = `
  241. <source src="VIDEO-SOURCE" type="video/VIDEO-TYPE">
  242. `;
  243.  
  244. // CSS to add to style elements
  245. const initialCSS = getInitialCSS();
  246.  
  247. const guiCSS = () => `
  248.  
  249. /* Fix for stubs leaving empty space at the bottom of the page for some reason */
  250. html {
  251. overflow-y: auto;
  252. }
  253.  
  254. body {
  255. overflow-y: clip;
  256. }
  257.  
  258. /* The settings widget */
  259. #${settingsWidgetId} {
  260. display: flex;
  261. float: right;
  262. bottom: 0;
  263. right: 0.8rem;
  264. max-width: 15rem;
  265. position: fixed;
  266. transform: translateY(${getData(WIDGET_HEIGHT_KEY) ?? storeData(WIDGET_HEIGHT_KEY, "21rem")});
  267. transition: 200ms ease-in-out transform;
  268. }
  269.  
  270. #${settingsWidgetId}:hover {
  271. transform: translateY(1px);
  272. }
  273.  
  274. /* The container for the widget */
  275. #widget-container {
  276. flex-direction: column;
  277. border: 1px solid #333;
  278. font-size: 0.9rem;
  279. background-color: ${widgetBgColor};
  280. width: 100%;
  281. padding: 1rem;
  282. border-radius: .5rem .5rem 0 0;
  283. margin-top: 50px; /* This is the space that will allow showing the popup when hovering over it */
  284. }
  285.  
  286. #widget-container:hover {
  287. margin-top: 0;
  288. }
  289.  
  290. /* Widget title */
  291. #widget-container > h1 {
  292. font-size: 1rem;
  293. font-weight: bold;
  294. height: 1rem;
  295. }
  296.  
  297. #widget-logo {
  298. display: inline-block;
  299. background-image: url('${logoImg}');
  300. height: 1rem;
  301. width: 1rem;
  302. background-size: contain;
  303. background-repeat: no-repeat;
  304. margin-right: 0.5rem;
  305. }
  306.  
  307. /* Widget block button */
  308. #block-tag-btn {
  309. appearance: none;
  310. color: #ddd;
  311. background: rgba(255,255,255,0.1);
  312. border: none;
  313. border-radius: 2rem;
  314. padding: .3rem .5rem;
  315. }
  316.  
  317. /* The layout for each label + form control */
  318. .form-entry {
  319. display: inline-flex;
  320. }
  321.  
  322. .form-entry:not(:first-child) {
  323. margin-top: 0.8rem;
  324. }
  325.  
  326. .form-entry > label {
  327. display: flex;
  328. align-items: center;
  329. justify-content: left;
  330. width: 10rem;
  331. }
  332.  
  333. .btn-container {
  334. display: grid;
  335. place-items: center;
  336. }
  337.  
  338. /* The dialog to filter tags */
  339. #${blockedTagsDialogId} {
  340. appearance: none;
  341. border: none;
  342. background-color: ${widgetBgColor};
  343. padding: 1rem;
  344. border-radius: 0.5rem;
  345. box-shadow: 0 0 20px 5px rgba(255,255,255,0.2);
  346. position: fixed;
  347. top: 50%;
  348. bottom: 50%;
  349. z-index: 1001;
  350. max-width: 36.5rem;
  351. }
  352.  
  353. #dialog-bg {
  354. display: none;
  355. background-color: rgba(0,0,0,.5); /* For browsers that don't support backdrop-filter */
  356. position: fixed;
  357. height: 100vh;
  358. width: 100vw;
  359. backdrop-filter: blur(2px);
  360. top: 0;
  361. left: 0;
  362. z-index: 1000;
  363. }
  364.  
  365. #blocked-tags-separator {
  366. width: 100%;
  367. margin-left: auto;
  368. margin-right: auto;
  369. border-top: 2px solid #666;
  370. margin-top: .5rem;
  371. margin-bottom: .5rem;
  372. }
  373.  
  374. #blocked-tags-dialog-head {
  375. display: flex;
  376. }
  377.  
  378. #blocked-tags-dialog-head > h1 {
  379. font-size: 1.5rem;
  380. margin-bottom: 1rem;
  381. color: #fff;
  382. }
  383.  
  384. #tag-input-container {
  385. margin-top: 1rem;
  386. margin-bottom: 1rem;
  387. display: flex;
  388. }
  389.  
  390. #blocked-tags-input-error {
  391. color: ${errorToastColor};
  392. font-size: 0.8rem;
  393. background-color: ${errorToastBgColor};
  394. padding: .4rem;
  395. border-radius: .5rem;
  396. font-weight: bold;
  397. }
  398.  
  399. #blocked-tags-field-container {
  400. margin-bottom: 1rem;
  401. }
  402.  
  403. #blocked-tags-field {
  404. display: grid;
  405. padding: 1rem;
  406. max-height: 20rem;
  407. overflow-y: auto;
  408. overflow-x: hidden;
  409. scrollbar-width: thin;
  410. place-items: center;
  411. border: 1px solid #555;
  412. border-radius: 0.3rem;
  413. }
  414.  
  415. #empty-blocked-tags {
  416. color: #666;
  417. margin: 0;
  418. padding: 1rem;
  419. }
  420.  
  421. #clipboard-notice {
  422. color: #2f2;
  423. font-size: 0.8rem;
  424. width: 100%;
  425. text-align: center;
  426. }
  427.  
  428. #tag-input {
  429. border: none;
  430. flex-grow: 100;
  431. background-color: #333;
  432. border-radius: .5rem;
  433. padding: .5rem 1rem;
  434. width: 80%;
  435. margin-right: 1rem;
  436. color: #ddd;
  437. }
  438.  
  439. #tag-save-btn {
  440. appearance: none;
  441. border: none;
  442. background-color: ${primaryBtnColor};
  443. color: #ddd;
  444. padding: .5rem 1rem;
  445. border-radius: 0.3rem;
  446. }
  447.  
  448. #tag-save-btn:hover {
  449. background-color: ${primaryBtnHoverColor};
  450. color: #fff;
  451. }
  452.  
  453. #tag-save-btn:active {
  454. background-color: ${primaryBtnActiveColor};
  455. color: #fff;
  456. }
  457.  
  458. #tag-case-insensitive-legend > span {
  459. background-color: ${caseInsensitiveTagColor};
  460. }
  461.  
  462. #tag-case-sensitive-legend > span {
  463. background-color: ${caseSensitiveTagColor};
  464. }
  465.  
  466. #blocked-tags-field-legend {
  467. display: inline-flex;
  468. align-items: center;
  469. justify-content: center;
  470. width: 100%;
  471. }
  472.  
  473. .close-dialog-btn {
  474. appearance: none;
  475. color: #fff;
  476. font-weight: bold;
  477. background-color: transparent;
  478. border: none;
  479. padding: 0.2rem 0.6rem;
  480. position: absolute;
  481. right: 0;
  482. top: 0;
  483. transition: 200ms all;
  484. }
  485.  
  486. .close-dialog-btn:hover {
  487. border-radius: 6rem;
  488. background-color: rgba(255,255,255,0.2);
  489. }
  490.  
  491. .switch-container {
  492. width: 100%;
  493. display: flex;
  494. }
  495.  
  496. .switch-container > label {
  497. width: 16rem;
  498. margin-right: 1rem;
  499. }
  500.  
  501. .blocked-tags-list {
  502. display: flex;
  503. flex-wrap: wrap;
  504. max-width: 35.5rem;
  505. margin-bottom: 0;
  506. padding: 0;
  507. }
  508.  
  509. .blocked-tags-list > li {
  510. display: inline-flex;
  511. list-style-type: none;
  512. }
  513.  
  514. .tag-legend {
  515. margin-right: 1rem;
  516. }
  517.  
  518. .tag-color-legend {
  519. display: inline-block;
  520. width: 7px;
  521. height: 7px;
  522. }
  523.  
  524. .tag-label-legend {
  525. font-size: 0.6rem;
  526. }
  527.  
  528. .blocked-tag {
  529. border-radius: 0.4rem;
  530. font-size: 0.8rem;
  531. color: #fff;
  532. margin: .2rem;
  533. cursor: grab;
  534. }
  535.  
  536. .blocked-tag[data-dragged-item] {
  537. cursor: grabbing;
  538. opacity: 0.7;
  539. }
  540.  
  541. .blocked-tag label {
  542. padding-left: .5rem;
  543. white-space: pre-wrap;
  544. cursor: grab;
  545. }
  546.  
  547. .blocked-tag button {
  548. appearance: none;
  549. color: #fff;
  550. background: transparent;
  551. border: none;
  552. border-radius: .4rem;
  553. }
  554.  
  555. .blocked-tag button:hover {
  556. background-color: rgba(255,255,255,0.2);
  557. }
  558.  
  559. .case-insensitive-tag {
  560. background-color: ${caseInsensitiveTagColor};
  561. }
  562.  
  563. .case-sensitive-tag {
  564. background-color: ${caseSensitiveTagColor};
  565. }
  566.  
  567. /* The filtered post stubs */
  568. .filtered-post-stub p {
  569. margin-bottom: 0;
  570. }
  571.  
  572. .hidden-post-stub-meta-container {
  573. display: flex;
  574. }
  575.  
  576. .hidden-post-stub-btn-container {
  577. margin-left: auto;
  578. }
  579.  
  580. .hidden-post-stub-btn-container > button {
  581. appearance: none;
  582. background-color: transparent;
  583. border: none;
  584. cursor: pointer;
  585. color: #3498db;
  586. margin-right: 1rem;
  587. }
  588.  
  589. .hide-post-btn {
  590. appearance: none;
  591. color: #f22;
  592. font-weight: bold;
  593. font-size: 2rem;
  594. line-height: 1rem;
  595. width: 1.8rem;
  596. background-color: transparent;
  597. border: none;
  598. padding: 0.2rem 0.6rem;
  599. height: 0;
  600. float: right;
  601. transition: 200ms all;
  602. }
  603.  
  604. /* The switch - the box around the slider */
  605. .switch {
  606. position: relative;
  607. display: inline-block;
  608. width: 30px;
  609. height: 16px;
  610. }
  611.  
  612. /* Hide default HTML checkbox */
  613. .switch input {
  614. opacity: 0;
  615. width: 0;
  616. height: 0;
  617. }
  618.  
  619. /* The slider */
  620. .slider {
  621. position: absolute;
  622. cursor: pointer;
  623. top: 0;
  624. left: 0;
  625. right: 0;
  626. bottom: 0;
  627. background-color: #777;
  628. -webkit-transition: .4s;
  629. transition: .4s;
  630. }
  631.  
  632. .slider:before {
  633. position: absolute;
  634. content: "";
  635. height: 12px;
  636. width: 12px;
  637. left: 2px;
  638. bottom: 2px;
  639. background-color: white;
  640. -webkit-transition: .4s;
  641. transition: .4s;
  642. }
  643.  
  644. input:checked + .slider {
  645. background-color: #2196F3;
  646. }
  647.  
  648. input:focus + .slider {
  649. box-shadow: 0 0 1px #2196F3;
  650. }
  651.  
  652. input:checked + .slider:before {
  653. -webkit-transform: translateX(12px);
  654. -ms-transform: translateX(12px);
  655. transform: translateX(12px);
  656. }
  657.  
  658. /* Rounded sliders */
  659. .slider.round {
  660. border-radius: 34px;
  661. }
  662.  
  663. .slider.round:before {
  664. border-radius: 50%;
  665. }
  666.  
  667. `;
  668.  
  669. const unblurCSS = `
  670.  
  671. /* Unblurs thumbnails */
  672. .img-blur {
  673. filter: none !important;
  674. }
  675.  
  676. `
  677.  
  678. //----------------------------------------
  679. // GUI AND INITIAL SETUP
  680. //----------------------------------------
  681.  
  682. // Load initial settings and store defaults if they didn't exist
  683. console.info(`Loading data for domain '${document.location.host}'.`);
  684.  
  685. const blockedTitleTags = getData(BLOCKED_TAGS_KEY) ?? storeData(BLOCKED_TAGS_KEY, []);
  686. const csBlockedTitleTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY) ?? storeData(BLOCKED_CASE_SENSITIVE_TAGS_KEY, []);
  687. const expandMediaPosts = getData(AUTO_OPEN_MEDIA_POSTS_KEY) ?? storeData(AUTO_OPEN_MEDIA_POSTS_KEY, false);
  688. const unblurThumbnails = getData(UNBLUR_THUMBNAILS_KEY) ?? storeData(UNBLUR_THUMBNAILS_KEY, false);
  689. const useCommasAsSeparators = getData(COMMAS_AS_SEPARATORS_KEY) ?? storeData(COMMAS_AS_SEPARATORS_KEY, true);
  690. const showStubsForFilteredPosts = getData(SHOW_FILTERED_STUBS_KEY) ?? storeData(SHOW_FILTERED_STUBS_KEY, true);
  691. const fixPostVideoPreviews = getData(FIX_BROKEN_VIDEO_PREVIEWS_KEY) ?? storeData(FIX_BROKEN_VIDEO_PREVIEWS_KEY, true);
  692. const caseSensitiveTags = getData(CASE_SENSITIVE_KEY) ?? storeData(CASE_SENSITIVE_KEY, false);
  693. const markNewPostsAsNSFW = getData(MARK_POSTS_AS_NSFW_KEY) ?? storeData(MARK_POSTS_AS_NSFW_KEY, false);
  694.  
  695. if (blockedTitleTags.length > 0 || csBlockedTitleTags.length > 0)
  696. console.info("Waiting for page to fully load to block tags: ", blockedTitleTags, csBlockedTitleTags);
  697.  
  698. // Add the GUI to control the settings to the document
  699. const settingWidget = addElement(document.body, "DIV", settingsWidgetId, bottomWidgetContent);
  700. const tagsDialog = addElement(document.body, "DIALOG", blockedTagsDialogId, tagsDialogContent);
  701. const dialogBg = addElement(document.body, "DIV", dialogBgId);
  702.  
  703. // Add initial CSS changes
  704. updateCSS();
  705.  
  706. // Load tags into the dialog window
  707. updateVisibleTags();
  708.  
  709. // Reflect boolean values of settings on GUI
  710. document.querySelector("#show-stub-swt > input").checked = showStubsForFilteredPosts;
  711. document.querySelector("#auto-open-swt > input").checked = expandMediaPosts;
  712. document.querySelector("#unblur-swt > input").checked = unblurThumbnails;
  713. document.querySelector("#use-commas-swt > input").checked = useCommasAsSeparators;
  714. document.querySelector("#case-sensitive-swt > input").checked = caseSensitiveTags;
  715. document.querySelector("#fix-video-previews-swt > input").checked = fixPostVideoPreviews;
  716. document.querySelector("#default-nsfw-posts-swt > input").checked = markNewPostsAsNSFW;
  717.  
  718. // --------- Add event listeners ---------
  719.  
  720. // Blocked list dialog button
  721. document.getElementById("block-tag-btn").onclick = () => {
  722. openDialog(tagsDialog);
  723. }
  724.  
  725. // Close dialog button
  726. tagsDialog.getElementsByClassName("close-dialog-btn")[0].onclick = () => {
  727. closeDialog(tagsDialog);
  728. }
  729.  
  730. // Close dialog on clicking outside the dialog
  731. dialogBg.onclick = (event) => {
  732. event.stopPropagation();
  733. closeDialog(tagsDialog);
  734. }
  735.  
  736. // Show stubs on filtering posts
  737. document.getElementById("show-stub-swt").onclick = (event) => {
  738. const showStubs = storeData(SHOW_FILTERED_STUBS_KEY, toggleCheckbox(event));
  739. const postsContainer = getPostContainer();
  740.  
  741. if (postsContainer)
  742. postsContainer.getElementsByClassName(postContainer).forEach(post => {
  743. let siblingNode = post.nextElementSibling;
  744. if (showStubs && post.hasAttribute("hidden")) {
  745. // Show sibling separator as well
  746. if (siblingNode && siblingNode.tagName == "HR")
  747. siblingNode.removeAttribute("hidden");
  748.  
  749. // Show post with the stub
  750. post.removeAttribute("hidden");
  751. } else if (post.getElementsByClassName("filtered-post-stub")[0]) {
  752.  
  753. // Remove separator if it exists
  754. if (siblingNode && siblingNode.tagName == "HR")
  755. siblingNode.setAttribute("hidden", true);
  756.  
  757. // Hide post completelly
  758. post.setAttribute("hidden", true);
  759. }
  760. })
  761. }
  762.  
  763. // Fix some broken video previews
  764. document.getElementById("fix-video-previews-swt").onclick = (event) => {
  765. const fixVideoPreviews = storeData(FIX_BROKEN_VIDEO_PREVIEWS_KEY, toggleCheckbox(event));
  766. const fixedPreviewContainers = document.getElementsByClassName(fixedPreviewContainerClass);
  767. const fixedPreviewVideos = document.getElementsByClassName(fixedPreviewVideoClass);
  768. const postsContainer = getPostContainer();
  769. const wasProcessed = postsContainer.id === processedPageChecker;
  770.  
  771. if (!postsContainer)
  772. return;
  773.  
  774. if (!wasProcessed)
  775. postsContainer.setAttribute("id", processedPageChecker);
  776.  
  777. if (fixVideoPreviews) {
  778. if (fixedPreviewContainers.length > 0 || fixedPreviewVideos.length > 0) {
  779. for(let i = 0; i < fixedPreviewContainers.length; i++) {
  780. fixedPreviewContainers[i].querySelector("picture").setAttribute("hidden", true);
  781. fixedPreviewVideos[i].removeAttribute("hidden");
  782. }
  783. } else {
  784. fixBrokenVideoPreviews(postsContainer);
  785. }
  786. } else if (fixedPreviewContainers.length > 0 || fixedPreviewVideos.length > 0) {
  787. for(let i = 0; i < fixedPreviewContainers.length; i++) {
  788. fixedPreviewContainers[i].querySelector("picture").removeAttribute("hidden");
  789. fixedPreviewVideos[i].setAttribute("hidden", true);
  790. }
  791. }
  792. }
  793.  
  794. // Auto-open tags on page reload
  795. document.getElementById("auto-open-swt").onclick = (event) => {
  796. const openMediaPosts = storeData(AUTO_OPEN_MEDIA_POSTS_KEY, toggleCheckbox(event));
  797. const postsContainer = getPostContainer();
  798. const wasProcessed = postsContainer.id === processedPageChecker;
  799.  
  800. if (!postsContainer)
  801. return;
  802.  
  803. if (!wasProcessed)
  804. postsContainer.setAttribute("id", processedPageChecker);
  805.  
  806. // Open posts if they weren't already
  807. if (openMediaPosts) {
  808. openPosts(postsContainer);
  809. }
  810. }
  811.  
  812. // Unblur setting
  813. document.getElementById("unblur-swt").onclick = (event) => {
  814. storeData(UNBLUR_THUMBNAILS_KEY, toggleCheckbox(event));
  815.  
  816. // Update CSS of site after having changed the setting
  817. updateCSS();
  818. }
  819.  
  820. // Mark new posts as NSFW by default
  821. document.getElementById("default-nsfw-posts-swt").onclick = (event) => {
  822. const markAsNSFW = storeData(MARK_POSTS_AS_NSFW_KEY, toggleCheckbox(event));
  823. const NSFWCheckbox = document.querySelector(postAsNSFW);
  824. const createPostForm = document.getElementById(createPostContainerId);
  825.  
  826. if (NSFWCheckbox && !NSFWCheckbox.checked && markAsNSFW && createPostForm)
  827. NSFWCheckbox.click();
  828. }
  829.  
  830. // Auto-open tags on page reload
  831. document.getElementById("use-commas-swt").onclick = (event) => {
  832. storeData(COMMAS_AS_SEPARATORS_KEY, toggleCheckbox(event));
  833. }
  834.  
  835. // Auto-open tags on page reload
  836. document.getElementById("case-sensitive-swt").onclick = (event) => {
  837. storeData(CASE_SENSITIVE_KEY, toggleCheckbox(event));
  838. }
  839.  
  840. // Accept pressing enter while in some input to send the data
  841. document.getElementById("tag-input").onkeydown = (event) => {
  842. if(event.keyCode === 13){
  843. document.getElementById("tag-save-btn").click();
  844. }
  845. }
  846.  
  847. // Event for tag save on button click or enter on input of tag blocking
  848. document.getElementById("tag-save-btn").onclick = () => {
  849. const tagInput = document.getElementById("tag-input");
  850. const errorEl = document.getElementById("blocked-tags-input-error");
  851. const clipboardNoticeEl = document.getElementById("clipboard-notice");
  852. const tagsToSubmit = document.getElementById("tag-input").value;
  853. clipboardNoticeEl.setAttribute("hidden", true);
  854.  
  855. if (tagsToSubmit.trim().length > 1) {
  856. const isCaseSensitive = getData(CASE_SENSITIVE_KEY);
  857. const splitOnCommas = getData(COMMAS_AS_SEPARATORS_KEY);
  858. let tagsAsArray = getTagsAsArray(tagsToSubmit, splitOnCommas);
  859. let tagsKey;
  860.  
  861. if (isCaseSensitive)
  862. tagsKey = BLOCKED_CASE_SENSITIVE_TAGS_KEY;
  863. else
  864. tagsKey = BLOCKED_TAGS_KEY;
  865.  
  866. const oldTags = getData(tagsKey);
  867.  
  868. tagsAsArray = tagsAsArray.filter(tag => !oldTags.includes(tag));
  869.  
  870. for(let i = 0; i < tagsAsArray.length; i++) {
  871. const keywordStart = /^(regex:|source:(regex(\((.*?)\):|:))*|regex\((.*?)\):)/ig;
  872. console.log(tagsAsArray[i].replace(keywordStart, ""));
  873. if (tagsAsArray[i].replace(keywordStart, "").trim().length === 0) {
  874. errorEl.removeAttribute("hidden");
  875. return;
  876. }
  877. }
  878.  
  879. // Hide the error message if it was visible
  880. if (errorEl.hasAttribute("hidden"))
  881. errorEl.setAttribute("hidden", true);
  882.  
  883. if (tagsAsArray.length > 0) {
  884. // Get the old array of tags and concatenate the new one
  885. const allTags = oldTags.concat(tagsAsArray);
  886.  
  887. // Store the new array of tags and update the tags to show
  888. storeData(tagsKey, allTags);
  889. addTagsToDialog(tagsAsArray, tagsKey);
  890.  
  891. // Hide empty tags element if it was visible and show the tags
  892. const blockedTagsListContainer = document.getElementById(blockTagListId);
  893. const csBlockedTagsListContainer = document.getElementById(csBlockTagListId);
  894.  
  895. // Update tag section visibility if required
  896. checkForEmptyTags(getData(BLOCKED_TAGS_KEY), getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY));
  897. }
  898.  
  899. // Clear input
  900. tagInput.value = "";
  901. } else {
  902. errorEl.removeAttribute("hidden");
  903. }
  904. }
  905.  
  906. //----------------------------------------
  907. // MAIN METHODS
  908. //----------------------------------------
  909.  
  910. // Wait for page to fully load to start doing things
  911. window.onload = () => {
  912. const baseContainer = document.getElementById("app");
  913. let NSFWCheckbox = document.querySelector(postAsNSFW);
  914. let createPostForm = document.getElementById(createPostContainerId);
  915. let searchContainer = document.getElementsByClassName(searchContainerClass)[0];
  916. let postsContainer = getPostContainer();
  917.  
  918. if (NSFWCheckbox && !NSFWCheckbox.checked && markNewPostsAsNSFW && createPostForm)
  919. NSFWCheckbox.click();
  920.  
  921. // Make sure that the widget height is correct
  922. updateWidgetHeightCSS();
  923.  
  924. // If there is a posts container in the page
  925. if (postsContainer) {
  926.  
  927. // Perform first filter
  928. if (blockedTitleTags.length > 0 || csBlockedTitleTags.length > 0) {
  929. console.info("Page loaded, filtering tags...");
  930. filterPosts(postsContainer);
  931. }
  932.  
  933. // Open remaining posts if enabled
  934. if (expandMediaPosts)
  935. openPosts(postsContainer);
  936.  
  937. // Fix video previews if there was any post
  938. if (fixPostVideoPreviews)
  939. fixBrokenVideoPreviews(postsContainer);
  940.  
  941. document.getElementsByClassName(postsContainer.className)[0].setAttribute("id", processedPageChecker);
  942. }
  943.  
  944. // Observe the changes of the page to know when to rethrow the filter method when the user changes the page
  945. const observer = new MutationObserver((e) => {
  946. if (document.getElementById(processedPageChecker))
  947. return;
  948.  
  949. const blockedTags = getData(BLOCKED_TAGS_KEY);
  950. const csBlockedTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY);
  951. const autoOpenMedia = getData(AUTO_OPEN_MEDIA_POSTS_KEY);
  952. const fixVideoPreviews = getData(FIX_BROKEN_VIDEO_PREVIEWS_KEY);
  953. const markAsNSFW = getData(MARK_POSTS_AS_NSFW_KEY);
  954.  
  955. searchContainer = document.getElementsByClassName(searchContainerClass)[0];
  956. NSFWCheckbox = document.querySelector(postAsNSFW);
  957. createPostForm = document.getElementById(createPostContainerId);
  958. postsContainer = getPostContainer();
  959.  
  960. // If on the creation post page, mark as NSFW the post
  961. if (NSFWCheckbox && !NSFWCheckbox.checked && markAsNSFW && createPostForm)
  962. NSFWCheckbox.click();
  963.  
  964. for (let i = 0; i < e.length; i++) {
  965. if (e[i].target.getElementsByClassName(editPostClass)[0])
  966. return;
  967.  
  968. let postEdit = e[i].target.getElementsByClassName("form-control")[0];
  969. let filteredPostStub = e[i].target.getElementsByClassName("filtered-post-stub")[0];
  970. let filteredPostBtn = e[i].target.getElementsByClassName("hide-post-btn")[0];
  971.  
  972. // Prevent filtering to retrigger if editting a post
  973. if (postEdit && searchContainer)
  974. return;
  975.  
  976. // If a post was already filtered don't add stubs again
  977. if (filteredPostBtn || filteredPostStub)
  978. continue;
  979.  
  980. // Perform actions if on one of the pages
  981. if (postsContainer) {
  982. if (blockedTags.length > 0 || csBlockedTags.length > 0) {
  983. console.info("Page reloaded, filtering new posts...");
  984. filterPosts(postsContainer);
  985. }
  986.  
  987. if (autoOpenMedia)
  988. openPosts(postsContainer);
  989.  
  990. if (fixVideoPreviews)
  991. fixBrokenVideoPreviews(postsContainer);
  992.  
  993. // Mark page as already processed
  994. document.getElementsByClassName(postsContainer.className)[0].setAttribute("id", processedPageChecker);
  995. break;
  996. }
  997. }
  998. });
  999.  
  1000. observer.observe(baseContainer, {subtree: true, childList: true});
  1001. }
  1002.  
  1003. // Gets initial CSS content
  1004. function getInitialCSS() {
  1005. let style = document.head.getElementsByTagName("style")[0];
  1006. if (!style) {
  1007. style = document.createElement('style');
  1008. document.head.appendChild(style);
  1009. return "";
  1010. }
  1011.  
  1012. return style.innerHTML;
  1013. }
  1014.  
  1015. // Gets the CSS from the script to be in effect
  1016. function getEffectiveCSS() {
  1017. let fullCSS = guiCSS();
  1018.  
  1019. // Check each setting that changes CSS apart from the GUI
  1020. if (getData(UNBLUR_THUMBNAILS_KEY))
  1021. fullCSS += unblurCSS;
  1022.  
  1023. return fullCSS;
  1024. }
  1025.  
  1026. // Splits tags on commas if necessary
  1027. function getTagsAsArray(tagsString, splitOnCommas=true) {
  1028. if (splitOnCommas) {
  1029. return tagsString.split(",");
  1030. }
  1031. return [tagsString];
  1032. }
  1033.  
  1034. // Gets the current page posts container
  1035. function getPostContainer() {
  1036. return document.getElementsByClassName(postsContainerClass)[0] ||
  1037. document.getElementsByClassName(profileContainerClass)[0] ||
  1038. document.getElementsByClassName(searchContainerClass)[0] ||
  1039. document.querySelector(".post");
  1040. }
  1041.  
  1042. // Adds an element to the document
  1043. function addElement(parentEl, elementTag, elementId, html="", idAsClass=false) {
  1044. let p = parentEl;
  1045. let newElement = document.createElement(elementTag);
  1046. if (idAsClass)
  1047. newElement.className = elementId;
  1048. else
  1049. newElement.setAttribute('id', elementId);
  1050. newElement.innerHTML = html;
  1051. p.appendChild(newElement);
  1052. return newElement;
  1053. }
  1054.  
  1055. // Adds the new tags to the dialog
  1056. function addTagsToDialog(tagArray, tagsType) {
  1057. let tagsContainerSelector = csBlockTagListId;
  1058.  
  1059. if (tagsType === BLOCKED_TAGS_KEY)
  1060. tagsContainerSelector = blockTagListId;
  1061.  
  1062. const blockedTagsListEl = document.getElementById(tagsContainerSelector);
  1063.  
  1064. tagArray.forEach(tag => {
  1065. addTagElement(tag, tagsType, blockedTagsListEl);
  1066. });
  1067. }
  1068.  
  1069. // Adds a tag element to the dialog
  1070. function addTagElement(tag, tagType, tagListElement) {
  1071.  
  1072. // Get the specific css class for tag type
  1073. let tagSpecificClass = "case-sensitive-tag";
  1074.  
  1075. if (tagType === BLOCKED_TAGS_KEY)
  1076. tagSpecificClass = "case-insensitive-tag";
  1077.  
  1078. // Add the element to the dialog and bind the button event
  1079. const newTag = addElement(tagListElement,
  1080. "LI",
  1081. `blocked-tag ${tagSpecificClass}`,
  1082. tagContent
  1083. .replace("TAG-NAME", parseTagName(tag))
  1084. .replace("TAG-TYPE", tagType)
  1085. .replace("TOOLTIP-CONTENT", `'${tag}' (${tagSpecificClass})`),
  1086. true);
  1087.  
  1088. newTag.getElementsByTagName("button")[0].onclick = () => {
  1089. onRemoveBlockedTag(tagType, tag, newTag);
  1090. }
  1091.  
  1092. newTag.getElementsByTagName("label")[0].onclick = () => {
  1093. GM_setClipboard(tag);
  1094. document.getElementById("clipboard-notice").removeAttribute("hidden");
  1095. }
  1096.  
  1097. // Mark element as draggable
  1098. newTag.setAttribute("draggable", true);
  1099.  
  1100. // Handle drag & drop events
  1101. newTag.ondragstart = (e) => {
  1102. newTag.setAttribute("data-dragged-item", true);
  1103. }
  1104.  
  1105. newTag.ondragover = (e) => {
  1106. e.preventDefault();
  1107. }
  1108.  
  1109. newTag.ondragend = () => {
  1110. newTag.removeAttribute("data-dragged-item");
  1111. }
  1112.  
  1113. newTag.ondrop = (e) => {
  1114. const target = e.target;
  1115. const targetTag = target.nodeName === "LABEL" || target.nodeName == "BUTTON" ?
  1116. target.parentNode.parentNode : target.parentNode;
  1117. const draggedItem = document.querySelector('li[data-dragged-item]');
  1118. const nextTagName = targetTag.getElementsByTagName("label")[0].innerHTML;
  1119. const movedTagName = draggedItem.getElementsByTagName("label")[0].innerHTML;
  1120. let movedTagParentId = targetTag.parentNode.id;
  1121. let draggedTagType = BLOCKED_TAGS_KEY;
  1122.  
  1123. if (draggedItem.className.includes("case-sensitive-tag"))
  1124. draggedTagType = BLOCKED_CASE_SENSITIVE_TAGS_KEY;
  1125.  
  1126. // Only accept dropping it in the same area
  1127. if (draggedItem.parentNode.id === movedTagParentId) {
  1128.  
  1129. // Update order of tags
  1130. let tags = getData(draggedTagType);
  1131.  
  1132. // We need to change a bit the logic depending on the position of the element
  1133. if (tags.indexOf(nextTagName) > tags.indexOf(movedTagName)) {
  1134. tags.splice(tags.indexOf(nextTagName) + 1, 0, null);
  1135. targetTag.parentNode.insertBefore(draggedItem, targetTag.nextSibling);
  1136. } else {
  1137. tags.splice(tags.indexOf(nextTagName), 0, null);
  1138. targetTag.parentNode.insertBefore(draggedItem, targetTag);
  1139. }
  1140.  
  1141. tags.splice(tags.indexOf(movedTagName), 1);
  1142. tags[tags.indexOf(null)] = movedTagName;
  1143.  
  1144. storeData(draggedTagType, tags);
  1145. }
  1146. newTag.removeAttribute('data-dragged-item');
  1147. }
  1148. }
  1149.  
  1150. // Updates the CSS of the site
  1151. function updateCSS() {
  1152. let style = document.head.getElementsByTagName("style")[0];
  1153. if (!style) {
  1154. style = document.createElement('style');
  1155. style.innerHTML = css;
  1156. document.head.appendChild(style);
  1157. return;
  1158. }
  1159.  
  1160. // Update the CSS with the initial one + the settings CSS
  1161. style.innerHTML = initialCSS + getEffectiveCSS();
  1162. }
  1163.  
  1164. // Update the visible part of the settings widget
  1165. function updateWidgetHeightCSS() {
  1166. const widget = document.getElementById(settingsWidgetContainerId);
  1167. const widgetHeight = widget.getBoundingClientRect().height;
  1168. const visibleTopHeight = 10;
  1169. if (widgetHeight > 0 && widgetHeight - visibleTopHeight > 0) {
  1170. const oldHeight = getData(WIDGET_HEIGHT_KEY);
  1171. let updatedHeight = (widgetHeight - visibleTopHeight) + "px";
  1172. if (oldHeight != updatedHeight) {
  1173. storeData(WIDGET_HEIGHT_KEY, (widgetHeight - visibleTopHeight) + "px");
  1174. updateCSS();
  1175. }
  1176. }
  1177. }
  1178.  
  1179. // Updates the visible tags in the blocked tags dialog
  1180. function updateVisibleTags() {
  1181. const csBlockedTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY);
  1182. const blockedTags = getData(BLOCKED_TAGS_KEY);
  1183. const blockedTagsListEl = document.getElementById(blockTagListId);
  1184. const csBlockedTagsListEl = document.getElementById(csBlockTagListId);
  1185.  
  1186. // Reset list
  1187. blockedTagsListEl.innerHTML = "";
  1188. csBlockedTagsListEl.innerHTML = "";
  1189.  
  1190. blockedTags.forEach(tag => {
  1191. addTagElement(tag, BLOCKED_TAGS_KEY, blockedTagsListEl);
  1192. });
  1193.  
  1194. csBlockedTags.forEach(tag => {
  1195. addTagElement(tag, BLOCKED_CASE_SENSITIVE_TAGS_KEY, csBlockedTagsListEl);
  1196. });
  1197.  
  1198. checkForEmptyTags(blockedTags, csBlockedTags);
  1199. }
  1200.  
  1201. // Parses the tag name if it's a named regex
  1202. function parseTagName(tag) {
  1203. if ((tag.toLowerCase().startsWith("regex(") || tag.toLowerCase().startsWith("source:regex(")) && new RegExp(namedRegex, "i").test(tag)) {
  1204. let regexName = getRegexNameFromTag(tag);
  1205. if (regexName != null) {
  1206. return tag.replace(new RegExp(namedRegex + ".*", "i"), `regex: ${regexName}`);
  1207. }
  1208. // If the named regex wasn't correctly formatted we just return the regular tag however it was typed
  1209. return tag;
  1210. }
  1211. return tag;
  1212. }
  1213.  
  1214. // Gets the regex name from a named regex tab
  1215. function getRegexNameFromTag(tag) {
  1216. let regexMatch = new RegExp(namedRegex).exec(tag);
  1217. if (regexMatch != null) {
  1218. return regexMatch[1];
  1219. }
  1220. return null;
  1221. }
  1222.  
  1223. // Toggles checkboxes on sliders inside the onclick event
  1224. function toggleCheckbox(event) {
  1225. let checkbox = event.target.parentNode.getElementsByTagName("input")[0];
  1226. checkbox.checked = !checkbox.checked;
  1227. return checkbox.checked;
  1228. }
  1229.  
  1230. // Opens a dialog box
  1231. function openDialog(dialogSelector) {
  1232. dialogSelector.show();
  1233. dialogBg.style.display = "block";
  1234. }
  1235.  
  1236. // Closes the dialog box
  1237. function closeDialog(dialogSelector) {
  1238. dialogSelector.close();
  1239. dialogBg.style.display = "none";
  1240. document.getElementById("blocked-tags-input-error").setAttribute("hidden", true);
  1241. document.getElementById("clipboard-notice").setAttribute("hidden", true);
  1242. }
  1243.  
  1244. // Adds the notice for no blocked tag if necessary
  1245. function checkForEmptyTags(blockedTags, csBlockedTags) {
  1246. const emptyTagsEl = document.getElementById("empty-blocked-tags");
  1247. const blockedTagsListEl = document.getElementById(blockTagListId);
  1248. const csBlockedTagsListEl = document.getElementById(csBlockTagListId);
  1249. const tagsSeparatorEl = document.getElementById("blocked-tags-separator");
  1250. const hasAnyBlockedTag = blockedTags.length > 0;
  1251. const hasAnyCsBlockedTag = csBlockedTags.length > 0;
  1252. const hasAnyTag = hasAnyBlockedTag || hasAnyCsBlockedTag;
  1253. const isBlockedTagsListHidden = blockedTagsListEl.hasAttribute("hidden");
  1254. const isCsBlockedTagsListHidden = csBlockedTagsListEl.hasAttribute("hidden");
  1255.  
  1256. // Check if any has tags
  1257. if (hasAnyTag) {
  1258. emptyTagsEl.setAttribute("hidden", true);
  1259. if (hasAnyBlockedTag)
  1260. blockedTagsListEl.removeAttribute("hidden");
  1261. else
  1262. blockedTagsListEl.setAttribute("hidden", true);
  1263.  
  1264. if (hasAnyCsBlockedTag)
  1265. csBlockedTagsListEl.removeAttribute("hidden");
  1266. else
  1267. csBlockedTagsListEl.setAttribute("hidden", true);
  1268.  
  1269. // Either both have tags or at least one has tags
  1270. if (hasAnyBlockedTag && hasAnyCsBlockedTag)
  1271. tagsSeparatorEl.removeAttribute("hidden");
  1272. else
  1273. tagsSeparatorEl.setAttribute("hidden", true);
  1274.  
  1275. // If it has no tags
  1276. } else if (!hasAnyTag) {
  1277. emptyTagsEl.removeAttribute("hidden");
  1278. blockedTagsListEl.setAttribute("hidden", true);
  1279. tagsSeparatorEl.setAttribute("hidden", true);
  1280. csBlockedTagsListEl.setAttribute("hidden", true);
  1281. }
  1282. }
  1283.  
  1284. // Checks if the page is loading
  1285. function checkIfPageIsLoading(postsContainer, posts) {
  1286. const searchContainer = document.getElementsByClassName(searchContainerClass)[0];
  1287. const emptyResultsContainer = document.querySelector(loadingSpinnerSelector);
  1288. return posts.length === 0 && postsContainer === searchContainer && emptyResultsContainer;
  1289. }
  1290.  
  1291. // Removes a tag from the view and the storeData
  1292. function onRemoveBlockedTag(tagType, tagToRemove, tagElement) {
  1293. // Remove the actual tag element
  1294. tagElement.parentNode.removeChild(tagElement);
  1295.  
  1296. // Update the storage
  1297. let allTags = getData(tagType);
  1298. let tagIndex = allTags.indexOf(tagToRemove);
  1299.  
  1300. if (tagIndex != -1)
  1301. allTags.splice(tagIndex, 1);
  1302.  
  1303. storeData(tagType, allTags);
  1304.  
  1305. // Toggle visibility of the elements where the tag was removed if they are empty
  1306. checkForEmptyTags(getData(BLOCKED_TAGS_KEY), getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY));
  1307. }
  1308.  
  1309. // Filters posts by words from the title as specified by the user
  1310. function filterPosts(postsContainer) {
  1311. let posts = postsContainer.getElementsByClassName(postContainer);
  1312.  
  1313. if (checkIfPageIsLoading(postsContainer, posts)) {
  1314. let intervalCount = 0;
  1315.  
  1316. // Check for posts in 1 second intervals
  1317. const checkInterval = setInterval(() => {
  1318. posts = document.getElementsByClassName(postContainer);
  1319. // Finish interval checking the search finished or 30 seconds passed
  1320. if (posts.length > 0 || intervalCount > 30 || !document.querySelector(loadingSpinnerSelector)) {
  1321. clearInterval(checkInterval);
  1322. filterPostsInPage(posts);
  1323. }
  1324. intervalCount++;
  1325. }, 1000);
  1326.  
  1327. return;
  1328. }
  1329.  
  1330. filterPostsInPage(posts);
  1331. }
  1332.  
  1333. // Filters the posts in the current page
  1334. function filterPostsInPage(postsList) {
  1335. const showStubs = getData(SHOW_FILTERED_STUBS_KEY);
  1336. const blockedTags = getData(BLOCKED_TAGS_KEY);
  1337. const csBlockedTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY);
  1338.  
  1339. const blockedTagsByType = {
  1340. byTitle: [],
  1341. bySource: []
  1342. }
  1343.  
  1344. const csBlockedTagsByType = {
  1345. byTitle: [],
  1346. bySource: []
  1347. }
  1348.  
  1349. // Get type of filter for each tag first to avoid unnecesary iterations
  1350. blockedTags.forEach(tag => {
  1351. if (tag.startsWith("source:"))
  1352. blockedTagsByType.bySource.push(tag);
  1353. else
  1354. blockedTagsByType.byTitle.push(tag);
  1355. })
  1356.  
  1357. csBlockedTags.forEach(tag => {
  1358. if (tag.startsWith("source:"))
  1359. csBlockedTagsByType.bySource.push(tag);
  1360. else
  1361. csBlockedTagsByType.byTitle.push(tag);
  1362. })
  1363.  
  1364. // Filter every post
  1365. Array.from(postsList).forEach(post => {
  1366. if (post.getElementsByClassName(editPostClass)[0])
  1367. return;
  1368.  
  1369. post.setAttribute(filteredPostChecker, true);
  1370. const communityEl = post.querySelector(postCommunityNameContainer);
  1371. const communityLinkEl = post.querySelector(postCommunityContainer);
  1372. const sourceEl = post.querySelector(postSrcLink)
  1373. let titleEl = post.querySelector(".post-title h1 span");
  1374. let source = null;
  1375. let community;
  1376. let communityLink;
  1377. let title;
  1378.  
  1379. // Make sure we get a valid title selector
  1380. if (!titleEl)
  1381. titleEl = post.querySelector(".post-title h1 a");
  1382. title = titleEl.innerHTML;
  1383.  
  1384. // Make sure community info is in post, otherwise get from page assuming the user
  1385. // is in the community view instead, where the community isn't in the posts but the page
  1386. if (!communityEl) {
  1387. community = document.querySelector(postCommunityNameContainer).innerHTML;
  1388. communityLink = document.querySelector(postCommunityContainer).href;
  1389. } else {
  1390. community = communityEl.innerHTML;
  1391. communityLink = communityLinkEl.href;
  1392. }
  1393.  
  1394. if (sourceEl)
  1395. source = sourceEl.href;
  1396.  
  1397. const postInfo = {
  1398. post: post,
  1399. title: title,
  1400. communityName: community,
  1401. communityLink: communityLink,
  1402. source: source
  1403. }
  1404.  
  1405. // Filter posts by title first
  1406. const wasFiltered = filterPost(blockedTagsByType.byTitle, csBlockedTagsByType.byTitle, showStubs, postInfo);
  1407.  
  1408. // Filter posts by source after if it wasn't already filtered by title
  1409. if (!wasFiltered && source)
  1410. filterPost(blockedTagsByType.bySource, csBlockedTagsByType.bySource, showStubs, postInfo, true);
  1411. })
  1412. }
  1413.  
  1414. function filterPost(blockedTags, csBlockedTags, showStubs, postInfo, filterBySource=false) {
  1415. // Filter posts using case insensitive tags
  1416. if (blockedTags.length > 0) {
  1417. for(let i = 0; i < blockedTags.length; i++) {
  1418. let tag = blockedTags[i];
  1419.  
  1420. if (filterBySource)
  1421. tag = tag.substring(7);
  1422.  
  1423. const regex = new RegExp(escapeRegex(tag), "i");
  1424. if (regex.test(filterBySource ? postInfo.source : postInfo.title)) {
  1425. removePost(postInfo, blockedTags[i], showStubs);
  1426.  
  1427. return true;
  1428. }
  1429. }
  1430. }
  1431.  
  1432. // Filter posts using case sensitive tags
  1433. if (csBlockedTags.length > 0) {
  1434. for(let i = 0; i < csBlockedTags.length; i++) {
  1435. const tag = csBlockedTags[i];
  1436.  
  1437. if (filterBySource)
  1438. tag = tag.substring(7);
  1439.  
  1440. const regex = new RegExp(escapeRegex(tag));
  1441. if (regex.test(filterBySource ? postInfo.source : postInfo.title)) {
  1442. removePost(postInfo, csBlockedTags[i], showStubs);
  1443. return true;
  1444. }
  1445. }
  1446. }
  1447. }
  1448.  
  1449. // Escapes the special characters entered by a user
  1450. function escapeRegex(regex) {
  1451. // Escape \'s in regex with double \\ if it's a regex to be used in "new RegExp()" method
  1452. if (regex.toLowerCase().startsWith("regex:"))
  1453. return regex.substring(6).replace("\\", "\\");
  1454.  
  1455. // If it's a named regex
  1456. else if (regex.toLowerCase().startsWith("regex(") && new RegExp(namedRegex, "i").test(regex)) {
  1457. const regStart = `regex(${getRegexNameFromTag(regex)}):`;
  1458. return regex.substring(regStart.length).replace("\\", "\\");
  1459. }
  1460. return regex.replace(/([()[{*+.$^\\|?])/g, '\\$1');
  1461.  
  1462. }
  1463.  
  1464. // Filters out a post
  1465. function removePost(postInfo, tag, showStubs) {
  1466. const post = postInfo.post;
  1467.  
  1468. // Hide the content of the children and add a stub by default
  1469. addFilterStubToPost(postInfo, tag)
  1470. hidePostChildren(post);
  1471.  
  1472. // If the setting to show stubs is disabled we hide the post completelly
  1473. if (!showStubs) {
  1474. // Removes the posts from the body completelly and logs it to the console
  1475. console.info(`Removing post with title ${postInfo.title} from community ${postInfo.communityName} because the post contained the tag ${parseTagName(tag)}.`);
  1476. post.setAttribute("hidden", true);
  1477.  
  1478. let siblingNode = post.nextElementSibling;
  1479.  
  1480. // Remove separator if it exists
  1481. if (siblingNode && siblingNode.tagName == "HR")
  1482. siblingNode.setAttribute("hidden", true);
  1483. }
  1484. }
  1485.  
  1486. // Hides all elements of the post children
  1487. function hidePostChildren(post) {
  1488. for (let i = 0; i < post.children.length; i++) {
  1489. let child = post.children[i];
  1490. if (child.className === "filtered-post-stub" || child.className === "hide-post-btn")
  1491. continue;
  1492. child.style.height = "0";
  1493. child.style.width = "0";
  1494. child.style.visibility = "hidden";
  1495. }
  1496. }
  1497.  
  1498. // Adds a filter stub after filtering a post
  1499. function addFilterStubToPost(postInfo, tag) {
  1500. let stub = addElement(postInfo.post, "DIV", "filtered-post-stub",
  1501. filteredPostStubContent.replace("TAG", parseTagName(tag))
  1502. .replace("POST-TITLE", postInfo.title)
  1503. .replace("POST-COMMUNITY", postInfo.communityName), true);
  1504. stub.getElementsByClassName("stub-hidden-post-community-link")[0].href = postInfo.communityLink;
  1505.  
  1506. // Add actions for the buttons
  1507. stub.getElementsByClassName("show-hidden-post-title-btn")[0].onclick = () => {
  1508. stub.getElementsByClassName("stub-hidden-post-title")[0].removeAttribute("hidden");
  1509. stub.getElementsByClassName("show-hidden-post-title-btn")[0].setAttribute("hidden", true);
  1510. }
  1511.  
  1512. stub.getElementsByClassName("show-hidden-post-btn")[0].onclick = () => {
  1513. stub.setAttribute("hidden", true);
  1514. postInfo.post.style= "";
  1515.  
  1516. // Make sure the title button element is hidden
  1517. stub.getElementsByClassName("stub-hidden-post-title")[0].setAttribute("hidden", true);
  1518. stub.getElementsByClassName("show-hidden-post-title-btn")[0].removeAttribute("hidden");
  1519.  
  1520. // Prepend the post re-remover if it didn't exist yet
  1521. let postRemoveEl = postInfo.post.getElementsByClassName("hide-post-btn")[0];
  1522. if (!postInfo.post.getElementsByClassName("hide-post-btn")[0]) {
  1523. postRemoveEl = document.createElement("BUTTON");
  1524. postRemoveEl.className = "hide-post-btn";
  1525. postRemoveEl.innerHTML = "🗶";
  1526. postRemoveEl.setAttribute("type", "button");
  1527. postInfo.post.prepend(postRemoveEl);
  1528.  
  1529. postRemoveEl.onclick = () => {
  1530. stub.removeAttribute("hidden");
  1531. postRemoveEl.setAttribute("hidden", true);
  1532. hidePostChildren(postInfo.post);
  1533. }
  1534. } else
  1535. postRemoveEl.removeAttribute("hidden");
  1536.  
  1537.  
  1538. for (let i = 0; i < postInfo.post.children.length; i++) {
  1539. postInfo.post.children[i].style = "";
  1540. }
  1541. }
  1542. }
  1543.  
  1544. // Opens media posts so that the image or video is shown for all posts in the current page of the timeline
  1545. function openPosts(postsContainer) {
  1546. let posts = postsContainer.getElementsByClassName(postContainer);
  1547.  
  1548. if (checkIfPageIsLoading(postsContainer, posts)) {
  1549. let intervalCount = 0;
  1550.  
  1551. // Check for posts in 1 second intervals
  1552. const checkInterval = setInterval(() => {
  1553. posts = document.getElementsByClassName(postContainer);
  1554.  
  1555. // Finish interval checking the search finished or 30 seconds passed
  1556. if (posts.length > 0 || intervalCount > 30 || !document.querySelector(loadingSpinnerSelector)) {
  1557. clearInterval(checkInterval);
  1558. clickPostsThumbnail(posts);
  1559. }
  1560. intervalCount++;
  1561. }, 1000);
  1562.  
  1563. return;
  1564. }
  1565.  
  1566. clickPostsThumbnail(posts)
  1567. }
  1568.  
  1569. // Clicks the thumbnail of the post to open it
  1570. function clickPostsThumbnail(posts) {
  1571. Array.from(posts).forEach(post => {
  1572. const postHasMedia = post.querySelector("picture, video");
  1573. let clickableThumbnail = post.querySelector("button.thumbnail");
  1574. if (postHasMedia) {
  1575. if (!clickableThumbnail)
  1576. clickableThumbnail = post.querySelector("a.text-body[href$='.mp4'] .thumbnail, a.text-body[href$='.webm'] .thumbnail, a.text-body[href^='https://www.redgifs.com/watch'] .thumbnail")
  1577.  
  1578. if (clickableThumbnail && !post.getElementsByClassName("filtered-post-stub")[0])
  1579. clickableThumbnail.click();
  1580. post.setAttribute(openedPostChecker, true);
  1581. }
  1582. })
  1583. }
  1584.  
  1585. // Fixes imgur previews showing as jpg instead of the actual video
  1586. function fixBrokenVideoPreviews(postsContainer) {
  1587. let posts = postsContainer.getElementsByClassName(postContainer);
  1588.  
  1589. if (checkIfPageIsLoading(postsContainer, posts)) {
  1590. let intervalCount = 0;
  1591.  
  1592. // Check for posts in 1 second intervals
  1593. const checkInterval = setInterval(() => {
  1594. posts = document.getElementsByClassName(postContainer);
  1595. // Finish interval checking the search finished or 30 seconds passed
  1596. if (posts.length > 0 || intervalCount > 30 || !document.querySelector(loadingSpinnerSelector)) {
  1597. clearInterval(checkInterval);
  1598. updateVideoPreviews(posts);
  1599. }
  1600. intervalCount++;
  1601. }, 1000);
  1602.  
  1603. return;
  1604. }
  1605. updateVideoPreviews(posts);
  1606. }
  1607.  
  1608. // Updates the video previews when needed so that they work again
  1609. function updateVideoPreviews(posts) {
  1610. Array.from(posts).forEach(post => {
  1611. const postSourceLinkEl = post.querySelector(postSrcLink);
  1612. const imageLinkContainer = post.querySelectorAll("div:nth-of-type(3) > a:not(.btn)");
  1613.  
  1614. if (postSourceLinkEl && imageLinkContainer.length > 0) {
  1615. const postSourceLink = postSourceLinkEl.href;
  1616. let newSrc = postSourceLink;
  1617.  
  1618. if (postSourceLink.includes("i.imgur.com") && postSourceLink.endsWith(".gifv"))
  1619. newSrc = postSourceLink.replace(".gifv", ".mp4");
  1620. else if (postSourceLink.includes("imgur.com"))
  1621. newSrc = postSourceLink.replace("imgur.com", "i.imgur.com") + ".mp4";
  1622.  
  1623. if (postSourceLink.includes("i.redd.it") && postSourceLink.endsWith(".gif"))
  1624. newSrc = imageLinkContainer[0].href;
  1625.  
  1626. // Only apply changes if the src was suposed to be different
  1627. if (newSrc != postSourceLink) {
  1628. imageLinkContainer.forEach(imageContainer => {
  1629. let pictureContainer = imageContainer.querySelector("picture");
  1630. if (pictureContainer) {
  1631. pictureContainer.setAttribute("hidden", true);
  1632. let videoElement = addElement(post.querySelector("div:nth-child(3).my-2"), "VIDEO",
  1633. `embed-responsive-item ${fixedPreviewVideoClass} col-12`,
  1634. videoSourceContent.replace("VIDEO-SOURCE", newSrc)
  1635. .replace("VIDEO-TYPE", newSrc.split(".").at(-1)),
  1636. true)
  1637. videoElement.setAttribute("controls", "");
  1638. videoElement.parentNode.classList.add("embed-responsive", fixedPreviewContainerClass);
  1639. }
  1640. });
  1641. }
  1642. }
  1643. });
  1644. }
  1645. //----------------------------------------
  1646. // STORAGE METHODS
  1647. //----------------------------------------
  1648.  
  1649. // Composes the key with the current instance name to store data per-instance
  1650. function composeInstanceKey(key) {
  1651. return document.location.host + "->" + key;
  1652. }
  1653.  
  1654. // Saves data to the storage of the userscript
  1655. function storeData(key, value) {
  1656. const instanceKey = composeInstanceKey(key);
  1657. let treatedValue = value;
  1658. if (typeof value === "array" || typeof value === "object")
  1659. treatedValue = JSON.stringify(value);
  1660. GM_setValue(instanceKey, treatedValue);
  1661. return treatedValue;
  1662. }
  1663.  
  1664. // Gets data from the userscript storage
  1665. function getData(key) {
  1666. const instanceKey = composeInstanceKey(key);
  1667. let value = GM_getValue(instanceKey);
  1668.  
  1669. if (value === null || value === undefined)
  1670. return null;
  1671.  
  1672. if (typeof value === "string") {
  1673. let isValueArray = value.startsWith("[") && value.endsWith("]");
  1674. let isValueObject = value.startsWith("{") && value.endsWith("}");
  1675. if (isValueArray || isValueObject)
  1676. return JSON.parse(value);
  1677. }
  1678. return value;
  1679. }

QingJ © 2025

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