Sharty Fixes 2025

Fixes/Enhancements for the 'party

当前为 2024-12-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Sharty Fixes 2025
  3. // @namespace soyjak.party
  4. // @match https://soyjak.party/*
  5. // @match https://soyjak.st/*
  6. // @version 1.11
  7. // @author Xyl (Currently Maintained by Swedewin)
  8. // @license MIT
  9. // @description Fixes/Enhancements for the 'party
  10. // ==/UserScript==
  11.  
  12. const version = "v1";
  13. console.log(`Sharty fixes ${version}`);
  14.  
  15. const namespace = "ShartyFixes.";
  16. function setValue(key, value) {
  17. if (key == "hiddenthreads" || key == "hiddenimages") {
  18. if (typeof GM_setValue == "function") {
  19. GM_setValue(key, value);
  20. }
  21. localStorage.setItem(key, value);
  22. } else {
  23. if (typeof GM_setValue == "function") {
  24. GM_setValue(namespace + key, value);
  25. } else {
  26. localStorage.setItem(namespace + key, value);
  27. }
  28. }
  29. }
  30.  
  31. function getValue(key) {
  32. if (key == "hiddenthreads" || key == "hiddenimages") {
  33. if (typeof GM_getValue == "function" && GM_getValue(key)) {
  34. localStorage.setItem(key, GM_getValue(key).toString());
  35. }
  36. return localStorage.getItem(key);
  37. }
  38. if (typeof GM_getValue == "function") {
  39. return GM_getValue(namespace + key);
  40. } else {
  41. return localStorage.getItem(namespace + key);
  42. }
  43. }
  44.  
  45. function isEnabled(key) {
  46. let value = getValue(key);
  47. if (value == null) {
  48. value = optionsEntries[key][2];
  49. setValue(key, value);
  50. }
  51. return value.toString() == "true";
  52. }
  53.  
  54. function getNumber(key) {
  55. let value = parseInt(getValue(key));
  56. if (Number.isNaN(value)) {
  57. value = 0;
  58. }
  59. return value;
  60. }
  61.  
  62. function getJson(key) {
  63. let value = getValue(key);
  64. if (value == null) {
  65. value = "{}";
  66. }
  67. return JSON.parse(value);
  68. }
  69.  
  70. function addToJson(key, jsonKey, value) {
  71. let json = getJson(key);
  72. let parent = json;
  73. jsonKey.split(".").forEach((e, index, array) => {
  74. if (index < array.length - 1) {
  75. if (!parent.hasOwnProperty(e)) {
  76. parent[e] = {};
  77. }
  78. parent = parent[e];
  79. } else {
  80. parent[e] = value;
  81. }
  82. });
  83. setValue(key, JSON.stringify(json));
  84. return json;
  85. }
  86.  
  87. function removeFromJson(key, jsonKey) {
  88. let json = getJson(key);
  89. let parent = json;
  90. jsonKey.split(".").forEach((e, index, array) => {
  91. if (index < array.length - 1) {
  92. parent = parent[e];
  93. } else {
  94. delete parent[e];
  95. }
  96. });
  97. setValue(key, JSON.stringify(json));
  98. return json;
  99. }
  100.  
  101. function customAlert(a) {
  102. document.body.insertAdjacentHTML("beforeend", `
  103. <div id="alert_handler">
  104. <div id="alert_background" onclick="this.parentNode.remove()"></div>
  105. <div id="alert_div">
  106. <a id='alert_close' href="javascript:void(0)" onclick="this.parentNode.parentNode.remove()"><i class='fa fa-times'></i></a>
  107. <div id="alert_message">${a}</div>
  108. <button class="button alert_button" onclick="this.parentNode.parentNode.remove()">OK</button>
  109. </div>
  110. </div>`);
  111. }
  112.  
  113. const optionsEntries = {
  114. "show-quote-button": ["checkbox", "Show quick quote button", false],
  115. "mass-reply-quote": ["checkbox", "Enable mass reply and mass quote buttons", true],
  116. "anonymise": ["checkbox", "Anonymise name and tripfags", false],
  117. "hide-blotter": ["checkbox", "Always hide blotter", false],
  118. "truncate-long-posts": ["checkbox", "Truncate line spam", true],
  119. "disable-submit-on-cooldown": ["checkbox", "Disable submit button on cooldown", false],
  120. "force-exact-time": ["checkbox", "Show exact time", false],
  121. "hide-sage-images": ["checkbox", "Hide sage images by default (help mitigate gross spam)", false],
  122. "catalog-navigation": ["checkbox", "Board list links to catalogs when on catalog", true]
  123. }
  124. let options = Options.add_tab("sharty-fixes", "gear", "Sharty Fixes").content[0];
  125. let optionsHTML = `<span style="display: block; text-align: center">${version}</span>`;
  126. optionsHTML += `<a style="display: block; text-align: center" href="https://booru.soyjak.st/post/list/variant%3Aimpish_soyak_ears/">#Impishgang</a><br>`;
  127. for ([optKey, optValue] of Object.entries(optionsEntries)) {
  128. optionsHTML += `<input type="${optValue[0]}" id="${optKey}" name="${optKey}"><label for="${optKey}">${optValue[1]}</label><br>`;
  129. }
  130. options.insertAdjacentHTML("beforeend", optionsHTML);
  131.  
  132. options.querySelectorAll("input[type=checkbox]").forEach(e => {
  133. e.checked = isEnabled(e.id);
  134. e.addEventListener("change", e => {
  135. setValue(e.target.id, e.target.checked);
  136. });
  137. });
  138.  
  139. // redirect (for some reason breaks hidden threads if removed)
  140. if (location.origin.match(/(http:|\/www)/g)) {
  141. location.replace(`https://soyjak.party${location.pathname}${location.hash}`);
  142. }
  143.  
  144. const board = window.location.pathname.split("/")[1];
  145.  
  146. // post fixes
  147. const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
  148. const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  149.  
  150. // Update observer for active thread
  151. if (document.body.classList.contains("active-thread")) {
  152. const updateObserver = new MutationObserver(list => {
  153. const evt = new CustomEvent("post_update", { detail: list });
  154. document.dispatchEvent(evt);
  155. });
  156. updateObserver.observe(document.querySelector(".thread"), { childList: true });
  157. }
  158.  
  159. let intervals = {};
  160.  
  161. // Fix individual post
  162. function fixPost(post) {
  163. let timeElement = post.querySelector("[datetime]");
  164. let time = new Date(Date.parse(timeElement.getAttribute("datetime")));
  165. let postNumber = post.getElementsByClassName("post_no")[1];
  166. let postText = postNumber.textContent;
  167.  
  168. // Hide images for sage posts
  169. if (email = post.querySelector("a.email")) {
  170. if (isEnabled("hide-sage-images") && !post.classList.contains("image-hide-processed") && email.href.match(/mailto:sage$/i)) {
  171. let localStorageBackup = localStorage.getItem("hiddenimages");
  172. let interval = setInterval(() => {
  173. if (document.querySelector(`[id*="${postText}"] .hide-image-link`)) {
  174. post.classList.add("image-hide-processed");
  175. clearInterval(interval);
  176. document.querySelector(`[id*="${postText}"] .files`)
  177. .querySelectorAll(".hide-image-link:not([style*='none'])")
  178. .forEach(e => e.click());
  179. localStorage.setItem("hiddenimages", localStorageBackup);
  180. }
  181. }, 50);
  182. }
  183. }
  184.  
  185. // Check if post is own post
  186. let isOwnPost = false;
  187. try {
  188. isOwnPost = JSON.parse(localStorage.getItem("own_posts"))[board].includes(post.querySelector(".post_no[onclick*=cite]").innerText);
  189. } catch { }
  190.  
  191. if (isOwnPost && getNumber("lastTime") < time.getTime()) {
  192. setValue("lastTime", time.getTime());
  193. }
  194.  
  195. // Format time
  196. timeElement.outerHTML = `<span datetime=${timeElement.getAttribute("datetime")}>${timeElement.innerText}</span>`;
  197. post.querySelector(".intro").insertAdjacentHTML("beforeend", `<span class="quote-buttons"></span>`);
  198.  
  199. // Add quote buttons if enabled
  200. if (isEnabled("show-quote-button")) {
  201. post.querySelector(".quote-buttons").insertAdjacentHTML("beforeend", `<a href="javascript:void(0);" class="quick-quote">[>]</a>`);
  202. post.querySelector(".quote-buttons").insertAdjacentHTML("beforeend", `<a href="javascript:void(0);" class="quick-orange">[<]</a>`);
  203. }
  204.  
  205. // Mass reply/quote options for OP posts
  206. if (isEnabled("mass-reply-quote") && post.classList.contains("op") && post.closest(".active-thread")) {
  207. document.querySelector(".quote-buttons").insertAdjacentHTML("beforeend", `<a href="javascript:void(0);" id="mass-reply">[Mass Reply]</a><a href="javascript:void(0);" id="mass-quote">[Mass Quote]</a><a href="javascript:void(0);" id="mass-orange">[Mass Orange]</a>`);
  208. }
  209.  
  210. // Handle post text formatting
  211. let body = post.querySelector(".body");
  212. body.childNodes.forEach(e => {
  213. if (e.nodeType === 3) {
  214. let span = document.createElement("span");
  215. span.innerText = e.textContent;
  216. e.parentNode.replaceChild(span, e);
  217. }
  218. });
  219.  
  220. // Format post number and add click event for citations
  221. if (document.body.classList.contains("active-thread")) {
  222. postNumber.href = `#q${postNumber.textContent}`;
  223. postNumber.setAttribute("onclick", `$(window).trigger('cite', [${postNumber.textContent}, null]);`);
  224. postNumber.addEventListener("click", () => {
  225. let selection = window.getSelection().toString();
  226. document.querySelectorAll("textarea[name=body]").forEach(e => {
  227. e.value += `>>${postNumber.textContent}\n${selection !== "" ? selection.replace(/(\r\n|\r|\n|^)/g, "$1>") : ""}`;
  228. });
  229. });
  230. }
  231.  
  232. // Anonymize post if enabled
  233. if (isEnabled("anonymise")) {
  234. post.querySelector(".name").textContent = "Chud";
  235. if (trip = post.querySelector(".trip")) {
  236. trip.remove();
  237. }
  238. }
  239.  
  240. // DO NOT REMOVE THIS, IT WILL BREAK THE SCRIPT.
  241. undoFilter(post);
  242. }
  243.  
  244. // Add expandos for truncating long posts
  245. function addExpandos() {
  246. if (isEnabled("truncate-long-posts")) {
  247. document.querySelectorAll(".post").forEach(e => {
  248. let body = e.querySelector(".body");
  249. e.classList.add("sf-cutoff");
  250.  
  251. if (body.scrollHeight > body.offsetHeight) {
  252. if (!e.querySelector(".sf-expander")) {
  253. body.insertAdjacentHTML("afterend", `<br><a href="javascript:void(0)" class="sf-expander"></a>`);
  254. }
  255. if (e.getAttribute("manual-cutoff") === "false" || (window.location.hash.includes(e.id.split("_")[1]) && !e.getAttribute("manual-cutoff"))) {
  256. e.classList.remove("sf-cutoff");
  257. }
  258. } else if (body.scrollHeight === body.offsetHeight) {
  259. if (expander = e.querySelector(".sf-expander")) {
  260. expander.remove();
  261. }
  262. e.classList.remove("sf-cutoff");
  263. }
  264. });
  265. }
  266. }
  267.  
  268. window.addEventListener("resize", () => addExpandos());
  269.  
  270. // Fix post times (relative and exact)
  271. function fixTime() {
  272. document.querySelectorAll(".post").forEach(e => {
  273. let timeElement = e.querySelector("[datetime]");
  274. let time = new Date(Date.parse(timeElement.getAttribute("datetime")));
  275. let exactTime = `${("0" + (time.getMonth() + 1)).slice(-2)}/${("0" + time.getDate()).slice(-2)}/${time.getYear().toString().slice(-2)} (${weekdays[time.getDay()]}) ${("0" + time.getHours()).slice(-2)}:${("0" + time.getMinutes()).slice(-2)}:${("0" + time.getSeconds()).slice(-2)}`;
  276. let relativeTime;
  277. let difference = (Date.now() - time.getTime()) / 1000;
  278.  
  279. if (difference < 10) relativeTime = "Just now";
  280. else if (difference < 60) relativeTime = `${Math.floor(difference)} seconds ago`;
  281. else if (difference < 120) relativeTime = `1 minute ago`;
  282. else if (difference < 3600) relativeTime = `${Math.floor(difference / 60)} minutes ago`;
  283. else if (difference < 7200) relativeTime = `1 hour ago`;
  284. else if (difference < 86400) relativeTime = `${Math.floor(difference / 3600)} hours ago`;
  285. else if (difference < 172800) relativeTime = `1 day ago`;
  286. else if (difference < 2678400) relativeTime = `${Math.floor(difference / 86400)} days ago`;
  287. else if (difference < 5356800) relativeTime = `1 month ago`;
  288. else if (difference < 31536000) relativeTime = `${Math.floor(difference / 2678400)} months ago`;
  289. else if (difference < 63072000) relativeTime = `1 year ago`;
  290. else relativeTime = `${Math.floor(difference / 31536000)} years ago`;
  291.  
  292. if (isEnabled("force-exact-time")) {
  293. timeElement.innerText = exactTime;
  294. timeElement.setAttribute("title", relativeTime);
  295. } else {
  296. timeElement.innerText = relativeTime;
  297. timeElement.setAttribute("title", exactTime);
  298. }
  299. });
  300. }
  301.  
  302. // Initialize post fixes
  303. function initFixes() {
  304. // Add formatting help to comment section
  305. document.querySelectorAll("form[name=post] th").forEach(e => {
  306. if (e.innerText === "Comment") {
  307. e.insertAdjacentHTML("beforeend", `<sup title="Formatting help" class="sf-formatting-help">?</sup><br><div class="comment-quotes"><a href="javascript:void(0);" class="comment-quote">[>]</a><a href="javascript:void(0);" class="comment-orange">[<]</a></div>`);
  308. }
  309. });
  310.  
  311. // Add file selection URL input if GM_xmlhttpRequest is available (i don't think this is needed anymore, removing or keeping it doesn't break anything though.)
  312. if (typeof GM_xmlhttpRequest === "function") {
  313. let fileSelectionInterval = setInterval(() => {
  314. if (select = document.querySelector("#upload_selection")) {
  315. select.childNodes[0].insertAdjacentHTML('afterend', ` / <a href="javascript:void(0)" id="sf-file-url"></a>`);
  316. clearInterval(fileSelectionInterval);
  317. }
  318. }, 100);
  319. }
  320.  
  321. // Handle dynamic updates
  322. document.addEventListener("dyn_update", e => {
  323. e.detail.forEach(e => fixPost(e));
  324. fixTime();
  325. addExpandos();
  326. });
  327.  
  328. // Handle post updates
  329. document.addEventListener("post_update", e => {
  330. e.detail.forEach(node => {
  331. if (node.addedNodes[0].nodeName === "DIV") {
  332. fixPost(node.addedNodes[0]);
  333. }
  334. });
  335. fixTime();
  336. addExpandos();
  337. });
  338.  
  339. // Apply fixes to existing posts
  340. [...document.getElementsByClassName("post")].forEach(e => {
  341. fixPost(e);
  342. });
  343. fixTime();
  344. addExpandos();
  345. }
  346.  
  347. // DONT REMOVE THIS PART, IT WILL BREAK THE SCRIPT
  348. // undo filter
  349. function undoFilter(post) {
  350. // if (isEnabled("restore-filtered")) {
  351. // post.querySelectorAll(".body, .body *, .replies, .replies *").forEach(e => {
  352. // e.childNodes.forEach(e => {
  353. // if (e.nodeName == "#text") {
  354. // e.nodeValue = e.nodeValue.replaceAll("im trans btw", "kuz");
  355. // }
  356. // });
  357. // });
  358. // }
  359. }
  360.  
  361.  
  362. // Catalog Fixes: Adjust thread timestamps & undo filters
  363. document.querySelectorAll("#Grid > div").forEach(e => {
  364. let threadTime = new Date(parseInt(e.getAttribute("data-time")) * 1000);
  365. e.getElementsByClassName("thread-image")[0].setAttribute("title", `${months[threadTime.getMonth()]} ${("0" + threadTime.getDate()).slice(-2)}` +
  366. ` ${("0" + threadTime.getHours()).slice(-2)}:${("0" + threadTime.getMinutes()).slice(-2)}`);
  367. undoFilter(e);
  368. });
  369.  
  370.  
  371. // Keyboard Shortcuts
  372. window.addEventListener("keydown", e => {
  373. if (e.key == "Enter" && (e.ctrlKey || e.metaKey)) {
  374. if (form = e.target.closest("form[name=post]")) {
  375. form.querySelector("input[type=submit]").click();
  376. }
  377. }
  378. });
  379.  
  380.  
  381. // Autofocus Textarea on Certain Pages
  382. if ((textarea = document.querySelector("textarea[name=body]")) && document.documentElement.classList.contains("desktop-style") && window.location.hash[1] != "q") {
  383. textarea.focus({
  384. preventScroll: true
  385. });
  386. }
  387.  
  388.  
  389. // Password Box Toggle (Show/Hide Password)
  390. if (passwordBox = document.querySelector("form[name=post] input[name=password]")) {
  391. passwordBox.setAttribute("type", "password");
  392. passwordBox.insertAdjacentHTML("afterend", `<input type="button" name="toggle-password" value="Show">`);
  393. }
  394.  
  395.  
  396. // Form Submit Cooldown
  397. document.querySelectorAll("form[name=post] input[type=submit]").forEach(e => {
  398. e.setAttribute("og-value", e.getAttribute("value"));
  399. });
  400.  
  401. setInterval(() => {
  402. let lastTime = getNumber("lastTime");
  403. let difference = 11 - Math.ceil((Date.now() - lastTime) / 1000);
  404. let buttons = document.querySelectorAll("form[name=post] input[type=submit]");
  405.  
  406. if ([...buttons].find(e => e.value.includes("Post"))) {
  407. return;
  408. } else if (difference > 0) {
  409. let disableButton = isEnabled("disable-submit-on-cooldown");
  410. buttons.forEach(e => {
  411. e.value = `${e.getAttribute("og-value")} (${difference})`;
  412. if (disableButton) {
  413. e.setAttribute("disabled", "disabled");
  414. }
  415. });
  416. } else {
  417. buttons.forEach(e => {
  418. e.value = e.getAttribute("og-value");
  419. e.removeAttribute("disabled");
  420. });
  421. }
  422. }, 100);
  423.  
  424.  
  425. // Thread Hiding Logic
  426. function areHiddenThreads() {
  427. let threadGrid = document.getElementById("Grid");
  428. if (document.querySelector(".catty-thread.hidden")) {
  429. if (!document.getElementById("toggle-hidden")) {
  430. document.querySelector(".desktop-style #image_size, .mobile-style header").insertAdjacentHTML("afterend", `<span id="toggle-hidden"></span>`);
  431. }
  432. } else if (toggleButton = document.getElementById("toggle-hidden")) {
  433. toggleButton.remove();
  434. document.body.classList.remove("showing-hidden");
  435. }
  436. }
  437.  
  438. // Catalog Navigation and Search Form
  439. if (document.body.classList.contains("active-catalog")) {
  440. if (isEnabled("catalog-navigation")) {
  441. document.querySelectorAll(".boardlist a[href*='index.html']").forEach(e => e.href = e.href.replace("index.html", "catalog.html"));
  442. }
  443.  
  444. document.querySelector("#image_size").insertAdjacentHTML("afterend", `
  445. <form style="display: inline-block; margin-bottom: 0px; float: right; margin-top: -13px;" action="/search.php">
  446. <p>
  447. <input type="text" name="search" placeholder="${board} search">
  448. <input type="hidden" name="board" value="${board}">
  449. <input type="submit" value="Search">
  450. </p>
  451. </form>
  452. `);
  453.  
  454. let hiddenThreads = getJson("hiddenthreads");
  455. let hasThreads = hiddenThreads.hasOwnProperty(board);
  456.  
  457. document.querySelectorAll(".mix").forEach(e => {
  458. e.classList.replace("mix", "catty-thread");
  459. if (hasThreads && hiddenThreads[board].hasOwnProperty(e.getAttribute("data-id"))) {
  460. e.classList.add("hidden");
  461. delete hiddenThreads[board][e.getAttribute("data-id")];
  462. }
  463. if (e.getAttribute("data-sticky") == "true") {
  464. e.parentNode.prepend(e);
  465. }
  466. });
  467.  
  468. if (hasThreads) {
  469. Object.keys(hiddenThreads[board]).forEach(e => {
  470. removeFromJson("hiddenthreads", `${board}.${e}`);
  471. });
  472. }
  473.  
  474. areHiddenThreads();
  475. }
  476.  
  477.  
  478. // Event Listeners for Clicks and Inputs
  479. document.addEventListener("click", e => {
  480. let t = e.target;
  481.  
  482. // Submit button in post form
  483. if (t.matches("form[name=post] input[type=submit]")) {
  484. t.value = t.getAttribute("og-value");
  485.  
  486. // Bypass filter and modify text
  487. if (isEnabled("bypass-filter")) {
  488. let textbox = t.closest("tbody").querySelector("textarea[name=body]");
  489. textbox.value = textbox.value.replaceAll(/(discord)/ig, str => {
  490. let arr = [];
  491. while (!arr.includes("​")) {
  492. arr = [];
  493. [...str].forEach((c, i) => {
  494. if (Math.random() < 0.5 && i != 0) {
  495. arr.push("​");
  496. }
  497. arr.push(c);
  498. if (Math.random() > 0.5 && i != str.length - 1) {
  499. arr.push("​");
  500. }
  501. });
  502. }
  503. return arr.join("");
  504. });
  505. }
  506. }
  507. // Toggle password visibility
  508. else if (t.matches("input[name=toggle-password]")) {
  509. if (passwordBox.getAttribute("type") == "password") {
  510. passwordBox.setAttribute("type", "text");
  511. t.value = "Hide";
  512. } else {
  513. passwordBox.setAttribute("type", "password");
  514. t.value = "Show";
  515. }
  516. }
  517. // Mass reply functionality
  518. else if (t.id == "mass-reply") {
  519. let massReply = "";
  520. document.querySelectorAll("[href*='#q']").forEach(e => {
  521. massReply += `>>${e.textContent}\n`;
  522. });
  523. document.querySelectorAll("textarea[name=body]").forEach(e => {
  524. e.value += massReply;
  525. e.focus();
  526. });
  527. }
  528.  
  529. // Mass quote or mass orange functionality
  530. else if (t.id == "mass-quote" || t.id == "mass-orange") {
  531. document.body.classList.add("hide-quote-buttons");
  532. let selection = window.getSelection();
  533. let range = document.createRange();
  534. range.selectNodeContents(document.body);
  535. selection.removeAllRanges();
  536. selection.addRange(range);
  537.  
  538. let massQuote = window.getSelection().toString().replace(/(\r\n|\r|\n|^)/g, t.id == "mass-quote" ? "$1>" : "$1<") + "\n";
  539. selection.removeAllRanges();
  540. document.body.classList.remove("hide-quote-buttons");
  541.  
  542. document.querySelectorAll("textarea[name=body]").forEach(e => {
  543. e.value += massQuote;
  544. e.focus();
  545. });
  546. }
  547.  
  548. // Quick quote or quick orange functionality
  549. else if (t.classList.contains("quick-quote") || t.classList.contains("quick-orange")) {
  550. let quote = t.closest(".post").querySelector(".body").innerText.replace(/(\r\n|\r|\n|^)/g, t.classList.contains("quick-quote") ? "$1>" : "$1<") + "\n";
  551. document.querySelectorAll("textarea[name=body]").forEach(e => {
  552. e.value += quote;
  553. e.focus();
  554. });
  555. }
  556.  
  557. // Comment quote or comment orange functionality
  558. else if (t.classList.contains("comment-quote") || t.classList.contains("comment-orange")) {
  559. document.querySelectorAll("textarea[name=body]").forEach(e => {
  560. e.value = e.value.replace(/(\r\n|\r|\n|^)/g, t.classList.contains("comment-quote") ? "$1>" : "$1<");
  561. e.focus();
  562. });
  563. }
  564.  
  565. // Toggle visibility of threads
  566. else if ((e.shiftKey || (e.detail == 3 && (document.documentElement.matches(".mobile-style") || isEnabled("desktop-triple-click")))) &&
  567. t.matches(".active-catalog .catty-thread *, .active-catalog .catty-thread")) {
  568. e.preventDefault();
  569. let thread = t.closest(".catty-thread");
  570. thread.classList.toggle("hidden");
  571.  
  572. if (thread.classList.contains("hidden")) {
  573. addToJson("hiddenthreads", `${board}.${thread.getAttribute("data-id")}`, Math.floor(Date.now() / 1000));
  574. } else {
  575. removeFromJson("hiddenthreads", `${board}.${thread.getAttribute("data-id")}`);
  576. }
  577. areHiddenThreads();
  578. }
  579.  
  580. // Toggle hidden threads visibility
  581. else if (t.id == "toggle-hidden") {
  582. document.body.classList.toggle("showing-hidden");
  583. }
  584.  
  585. // Hide/unhide thread link functionality
  586. else if (t.classList.contains("hide-thread-link") || t.classList.contains("unhide-thread-link")) {
  587. setValue("hiddenthreads", localStorage.getItem("hiddenthreads"));
  588. }
  589.  
  590. // Hide blotter functionality
  591. else if (t.classList.contains("hide-blotter")) {
  592. setValue("hidden-blotter", document.querySelector(".blotter").innerText);
  593. document.body.classList.add("hidden-blotter");
  594. }
  595.  
  596. // SF-expander button functionality (manual cutoff)
  597. else if (t.classList.contains("sf-expander")) {
  598. t.closest(".post").setAttribute("manual-cutoff", t.closest(".post").classList.toggle("sf-cutoff"));
  599. }
  600.  
  601. // Formatting help popup <span class="rotate">
  602. else if (t.classList.contains("sf-formatting-help")) {
  603. let help = `
  604. <span class="spoiler">**Spoiler**</span><br>
  605. <em>' 'Italics' '</em><br>
  606. <b>'''Bold'''</b><br>
  607. <code>\`\`\`Codetext\`\`\`</code><br>
  608. <u>__Underline__</u><br>
  609. <s>~~Strikethrough~~</s><br>
  610. <big>+=Bigtext=+</big><br>
  611. <span class="rotate">##Spintext##</span><br>
  612. <span class="quote">&gt;Greentext</span><br>
  613. <span class="quote2">&lt;Orangetext</span><br>
  614. <span class="quote3" style="color: #6577E6;">^Bluetext</span><br>
  615. <span class="heading">==Redtext==</span><br>
  616. <span class="heading2">--Bluetext--</span><br>
  617. <font color="FD3D98"><b>-~-Pinktext-~-</b></font><br>
  618. <span class="glow">%%Glowtext%%</span><br>
  619. <span style="text-shadow:0px 0px 40px #36d7f7, 0px 0px 2px #36d7f7">;;Blueglowtext;;</span><br>
  620. <span style="text-shadow:0px 0px 40px #fffb00, 0px 0px 2px #fffb00">::Yellowglowtext::</span><br>
  621. <span style="text-shadow:0px 0px 40px #ff0000, 0px 0px 2px #ff0000">!!Redglowtext!!</span> (put after each word)<br>
  622. <span style="background: linear-gradient(to left, red, orange , yellow, green, cyan, blue, violet);-webkit-background-clip: text;-webkit-text-fill-color: transparent;">~-~Rainbowtext~-~</span><br>
  623. <span class="glow"><span style="background: linear-gradient(to left, red, orange , yellow, green, cyan, blue, violet);-webkit-background-clip: text;-webkit-text-fill-color: transparent;">%%~-~Gemeraldtext~-~%%</span></span><br>
  624. <span style="background:#faf8f8;color:#3060a8">(((Zionisttext)))</span>
  625. `;
  626. customAlert(help);
  627. }
  628. });
  629.  
  630. // Search Input Filter for Catalog Threads
  631. document.addEventListener("input", e => {
  632. let t = e.target;
  633. if (t.matches("input[name=search]") && document.querySelector(".sf-catty, .active-catalog")) {
  634. document.querySelectorAll(".catty-thread").forEach(e => {
  635. if (e.innerText.toLowerCase().includes(t.value.toLowerCase())) {
  636. e.classList.remove("sf-filtered");
  637. } else {
  638. e.classList.add("sf-filtered");
  639. }
  640. });
  641. }
  642. });
  643.  
  644.  
  645. // Blotter Hide/Show Button
  646. if (blotter = document.querySelector(".blotter")) {
  647. blotter.insertAdjacentHTML("beforebegin", `<a class="hide-blotter" href="javascript:void(0)">[–]</a>`);
  648. if (blotter.innerText == getValue("hidden-blotter") || isEnabled("hide-blotter")) {
  649. document.body.classList.add("hidden-blotter");
  650. }
  651. }
  652.  
  653. document.head.insertAdjacentHTML("beforeend", `
  654. <style>
  655. /* Hide elements in specific conditions */
  656. .hide-blotter {
  657. float: left;
  658. }
  659.  
  660. .hidden-blotter .blotter,
  661. .hidden-blotter .blotter + hr,
  662. .hidden-blotter .hide-blotter,
  663. .catty-thread.hidden,
  664. .showing-hidden .catty-thread,
  665. .mobile-style .g-recaptcha-bubble-arrow,
  666. .catty-thread.sf-filtered,
  667. .showing-hidden .catty-thread.hidden.sf-filtered,
  668. .hide-quote-buttons .quote-buttons {
  669. display: none !important;
  670. }
  671.  
  672. /* Styling for expander button in replies */
  673. .reply .sf-expander {
  674. margin-left: 1.8em;
  675. padding-right: 3em;
  676. padding-bottom: 0.3em;
  677. }
  678.  
  679. .sf-expander::after {
  680. content: "[Hide Full Text]";
  681. }
  682.  
  683. .sf-cutoff .sf-expander::after {
  684. content: "[Show Full Text]";
  685. }
  686.  
  687. /* File URL input styling */
  688. #sf-file-url::after {
  689. content: "URL";
  690. }
  691.  
  692. #sf-file-url.sf-loading::after {
  693. content: "Loading...";
  694. }
  695.  
  696. /* Hover styling for sharty */
  697. #sharty-hover {
  698. pointer-events: none;
  699. position: fixed;
  700. z-index: 500;
  701. }
  702.  
  703. /* Display adjustments for threads */
  704. .catty-thread,
  705. .showing-hidden .catty-thread.hidden {
  706. display: inline-block !important;
  707. }
  708.  
  709. /* Toggle hidden threads button styling */
  710. #toggle-hidden {
  711. text-decoration: underline;
  712. color: #34345C;
  713. cursor: pointer;
  714. user-select: none;
  715. }
  716.  
  717. #toggle-hidden::before {
  718. content: "[Show Hidden]";
  719. }
  720.  
  721. .showing-hidden #toggle-hidden::before {
  722. content: "[Hide Hidden]";
  723. }
  724.  
  725. #image_size + #toggle-hidden {
  726. display: inline-block;
  727. padding-left: 5px;
  728. }
  729.  
  730. header + #toggle-hidden {
  731. display: block;
  732. margin: 1em auto;
  733. width: fit-content;
  734. }
  735.  
  736. /* Recaptcha bubble styling on mobile */
  737. .mobile-style .g-recaptcha-bubble-arrow + div {
  738. position: fixed !important;
  739. left: 50%;
  740. top: 50%;
  741. transform: translate(-50%, -50%);
  742. -webkit-transform: translate(-50%, -50%);
  743. }
  744.  
  745. /* Password input field adjustments */
  746. input[name=toggle-password] {
  747. margin-left: 2px;
  748. }
  749.  
  750. /* Formatting help link styling */
  751. .sf-formatting-help {
  752. text-decoration: underline;
  753. cursor: pointer;
  754. }
  755.  
  756. /* Comment quote button styling */
  757. .comment-quotes {
  758. text-align: center;
  759. }
  760.  
  761. /* Truncate long posts styling (conditionally enabled) */
  762. ${isEnabled("truncate-long-posts") ? `
  763. .sf-cutoff:not(.post-hover) .body {
  764. overflow: hidden;
  765. word-break: break-all;
  766. display: -webkit-box;
  767. min-width: min-content;
  768. -webkit-line-clamp: 20;
  769. -webkit-box-orient: vertical;
  770. }
  771.  
  772. .sf-cutoff.post-hover .sf-expander {
  773. display: none !important;
  774. }
  775.  
  776. div.post.reply.sf-cutoff div.body {
  777. margin-bottom: 0.3em;
  778. padding-bottom: unset;
  779. }
  780. ` : ""}
  781. </style>
  782. `);
  783.  
  784. // Initialize additional fixes
  785. initFixes();