Sharty Fixes 2025

Fixes/Enhancements for the 'party

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

  1. // ==UserScript==
  2. // @name Sharty Fixes 2025
  3. // @namespace soyjak.party
  4. // @match https://soyjak.party/*
  5. // @match https://soyjak.st/*
  6. // @match https://swedishwin.com/*
  7. // @version 1.150
  8. // @author Xyl (Currently Maintained by Swedewin)
  9. // @license MIT
  10. // @description Fixes/Enhancements for the 'party
  11. // ==/UserScript==
  12.  
  13. const version = "v1.150";
  14. console.log(`Sharty fixes ${version}`);
  15.  
  16. const namespace = "ShartyFixes.";
  17. function setValue(key, value) {
  18. if (key == "hiddenthreads" || key == "hiddenimages") {
  19. if (typeof GM_setValue == "function") {
  20. GM_setValue(key, value);
  21. }
  22. localStorage.setItem(key, value);
  23. } else {
  24. if (typeof GM_setValue == "function") {
  25. GM_setValue(namespace + key, value);
  26. } else {
  27. localStorage.setItem(namespace + key, value);
  28. }
  29. }
  30. }
  31.  
  32. function getValue(key) {
  33. if (key == "hiddenthreads" || key == "hiddenimages") {
  34. if (typeof GM_getValue == "function" && GM_getValue(key)) {
  35. localStorage.setItem(key, GM_getValue(key).toString());
  36. }
  37. return localStorage.getItem(key);
  38. }
  39. if (typeof GM_getValue == "function") {
  40. return GM_getValue(namespace + key);
  41. } else {
  42. return localStorage.getItem(namespace + key);
  43. }
  44. }
  45.  
  46. function isEnabled(key) {
  47. let value = getValue(key);
  48. if (value == null) {
  49. value = optionsEntries[key][2];
  50. setValue(key, value);
  51. }
  52. return value.toString() == "true";
  53. }
  54.  
  55. function getNumber(key) {
  56. let value = parseInt(getValue(key));
  57. if (Number.isNaN(value)) {
  58. value = 0;
  59. }
  60. return value;
  61. }
  62.  
  63. function getJson(key) {
  64. let value = getValue(key);
  65. if (value == null) {
  66. value = "{}";
  67. }
  68. return JSON.parse(value);
  69. }
  70.  
  71. function addToJson(key, jsonKey, value) {
  72. let json = getJson(key);
  73. let parent = json;
  74. jsonKey.split(".").forEach((e, index, array) => {
  75. if (index < array.length - 1) {
  76. if (!parent.hasOwnProperty(e)) {
  77. parent[e] = {};
  78. }
  79. parent = parent[e];
  80. } else {
  81. parent[e] = value;
  82. }
  83. });
  84. setValue(key, JSON.stringify(json));
  85. return json;
  86. }
  87.  
  88. function removeFromJson(key, jsonKey) {
  89. let json = getJson(key);
  90. let parent = json;
  91. jsonKey.split(".").forEach((e, index, array) => {
  92. if (index < array.length - 1) {
  93. parent = parent[e];
  94. } else {
  95. delete parent[e];
  96. }
  97. });
  98. setValue(key, JSON.stringify(json));
  99. return json;
  100. }
  101.  
  102. function customAlert(a) {
  103. document.body.insertAdjacentHTML("beforeend", `
  104. <div id="alert_handler">
  105. <div id="alert_background" onclick="this.parentNode.remove()"></div>
  106. <div id="alert_div">
  107. <a id='alert_close' href="javascript:void(0)" onclick="this.parentNode.parentNode.remove()"><i class='fa fa-times'></i></a>
  108. <div id="alert_message">${a}</div>
  109. <button class="button alert_button" onclick="this.parentNode.parentNode.remove()">OK</button>
  110. </div>
  111. </div>`);
  112. }
  113.  
  114. const optionsEntries = {
  115. "show-quote-button": ["checkbox", "Show quick quote button", false],
  116. "mass-reply-quote": ["checkbox", "Enable mass reply and mass quote buttons", true],
  117. "anonymise": ["checkbox", "Anonymise name and tripfags", false],
  118. "hide-blotter": ["checkbox", "Always hide blotter", false],
  119. "truncate-long-posts": ["checkbox", "Truncate line spam", true],
  120. "disable-submit-on-cooldown": ["checkbox", "Disable submit button on cooldown", false],
  121. "force-exact-time": ["checkbox", "Show exact time", false],
  122. "hide-sage-images": ["checkbox", "Hide sage images by default (help mitigate gross spam)", false],
  123. "catalog-navigation": ["checkbox", "Board list links to catalogs when on catalog", true]
  124. }
  125. let options = Options.add_tab("sharty-fixes", "gear", "Sharty Fixes").content[0];
  126. let optionsHTML = `<span style="display: block; text-align: center">${version}</span>`;
  127. optionsHTML += `<a style="display: block; text-align: center" href="https://booru.soyjak.st/post/list/variant%3Aimpish_soyak_ears/">#Impishgang</a><br>`;
  128. for ([optKey, optValue] of Object.entries(optionsEntries)) {
  129. optionsHTML += `<input type="${optValue[0]}" id="${optKey}" name="${optKey}"><label for="${optKey}">${optValue[1]}</label><br>`;
  130. }
  131. options.insertAdjacentHTML("beforeend", optionsHTML);
  132.  
  133. options.querySelectorAll("input[type=checkbox]").forEach(e => {
  134. e.checked = isEnabled(e.id);
  135. e.addEventListener("change", e => {
  136. setValue(e.target.id, e.target.checked);
  137. });
  138. });
  139.  
  140. // redirect (for some reason breaks hidden threads if removed)
  141. if (location.origin.match(/(http:|\/www)/g)) {
  142. location.replace(`https://soyjak.party${location.pathname}${location.hash}`);
  143. }
  144.  
  145. const board = window.location.pathname.split("/")[1];
  146.  
  147. // post fixes
  148. const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
  149. const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  150.  
  151. // Update observer for active thread
  152. if (document.body.classList.contains("active-thread")) {
  153. const updateObserver = new MutationObserver(list => {
  154. const evt = new CustomEvent("post_update", { detail: list });
  155. document.dispatchEvent(evt);
  156. });
  157. updateObserver.observe(document.querySelector(".thread"), { childList: true });
  158. }
  159.  
  160. let intervals = {};
  161.  
  162. // Fix individual post
  163. function fixPost(post) {
  164. let timeElement = post.querySelector("[datetime]");
  165. let time = new Date(Date.parse(timeElement.getAttribute("datetime")));
  166. let postNumber = post.getElementsByClassName("post_no")[1];
  167. let postText = postNumber.textContent;
  168.  
  169. // Hide images for sage posts
  170. if (email = post.querySelector("a.email")) {
  171. if (isEnabled("hide-sage-images") && !post.classList.contains("image-hide-processed") && email.href.match(/mailto:sage$/i)) {
  172. let localStorageBackup = localStorage.getItem("hiddenimages");
  173. let interval = setInterval(() => {
  174. if (document.querySelector(`[id*="${postText}"] .hide-image-link`)) {
  175. post.classList.add("image-hide-processed");
  176. clearInterval(interval);
  177. document.querySelector(`[id*="${postText}"] .files`)
  178. .querySelectorAll(".hide-image-link:not([style*='none'])")
  179. .forEach(e => e.click());
  180. localStorage.setItem("hiddenimages", localStorageBackup);
  181. }
  182. }, 50);
  183. }
  184. }
  185.  
  186. // Check if post is own post
  187. let isOwnPost = false;
  188. try {
  189. isOwnPost = JSON.parse(localStorage.getItem("own_posts"))[board].includes(post.querySelector(".post_no[onclick*=cite]").innerText);
  190. } catch { }
  191.  
  192. if (isOwnPost && getNumber("lastTime") < time.getTime()) {
  193. setValue("lastTime", time.getTime());
  194. }
  195.  
  196. // Format time
  197. timeElement.outerHTML = `<span datetime=${timeElement.getAttribute("datetime")}>${timeElement.innerText}</span>`;
  198. post.querySelector(".intro").insertAdjacentHTML("beforeend", `<span class="quote-buttons"></span>`);
  199.  
  200. // Add quote buttons if enabled
  201. if (isEnabled("show-quote-button")) {
  202. post.querySelector(".quote-buttons").insertAdjacentHTML("beforeend", `<a href="javascript:void(0);" class="quick-quote">[>]</a>`);
  203. post.querySelector(".quote-buttons").insertAdjacentHTML("beforeend", `<a href="javascript:void(0);" class="quick-orange">[<]</a>`);
  204. }
  205.  
  206. // Mass reply/quote options for OP posts
  207. if (isEnabled("mass-reply-quote") && post.classList.contains("op") && post.closest(".active-thread")) {
  208. 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>`);
  209. }
  210.  
  211. // Handle post text formatting
  212. let body = post.querySelector(".body");
  213. body.childNodes.forEach(e => {
  214. if (e.nodeType === 3) {
  215. let span = document.createElement("span");
  216. span.innerText = e.textContent;
  217. e.parentNode.replaceChild(span, e);
  218. }
  219. });
  220.  
  221. // Format post number and add click event for citations
  222. if (document.body.classList.contains("active-thread")) {
  223. postNumber.href = `#q${postNumber.textContent}`;
  224. postNumber.setAttribute("onclick", `$(window).trigger('cite', [${postNumber.textContent}, null]);`);
  225. postNumber.addEventListener("click", () => {
  226. let selection = window.getSelection().toString();
  227. document.querySelectorAll("textarea[name=body]").forEach(e => {
  228. e.value += `>>${postNumber.textContent}\n${selection !== "" ? selection.replace(/(\r\n|\r|\n|^)/g, "$1>") : ""}`;
  229. });
  230. });
  231. }
  232.  
  233. // Anonymize post if enabled
  234. if (isEnabled("anonymise")) {
  235. post.querySelector(".name").textContent = "Chud";
  236. if (trip = post.querySelector(".trip")) {
  237. trip.remove();
  238. }
  239. }
  240.  
  241. // DO NOT REMOVE THIS, IT WILL BREAK THE SCRIPT.
  242. undoFilter(post);
  243. }
  244.  
  245. // Add expandos for truncating long posts
  246. function addExpandos() {
  247. if (isEnabled("truncate-long-posts")) {
  248. document.querySelectorAll(".post").forEach(e => {
  249. let body = e.querySelector(".body");
  250. e.classList.add("sf-cutoff");
  251.  
  252. if (body.scrollHeight > body.offsetHeight) {
  253. if (!e.querySelector(".sf-expander")) {
  254. body.insertAdjacentHTML("afterend", `<br><a href="javascript:void(0)" class="sf-expander"></a>`);
  255. }
  256. if (e.getAttribute("manual-cutoff") === "false" || (window.location.hash.includes(e.id.split("_")[1]) && !e.getAttribute("manual-cutoff"))) {
  257. e.classList.remove("sf-cutoff");
  258. }
  259. } else if (body.scrollHeight === body.offsetHeight) {
  260. if (expander = e.querySelector(".sf-expander")) {
  261. expander.remove();
  262. }
  263. e.classList.remove("sf-cutoff");
  264. }
  265. });
  266. }
  267. }
  268.  
  269. window.addEventListener("resize", () => addExpandos());
  270.  
  271. // Function to modify the report form (File label, Urgent label, and Reason box)
  272. function modifyReportForm(form) {
  273. // Skip modification if the form has already been modified
  274. if (form.dataset.modified === 'true') return;
  275.  
  276. // Modify the "File" label and checkbox
  277. const fileLabel = form.querySelector('label[for^="delete_file_"]');
  278. if (fileLabel) {
  279. fileLabel.textContent = '[ File only ]'; // Modify the label text
  280. const fileCheckbox = form.querySelector('input[type="checkbox"][name="file"]');
  281. if (fileCheckbox) {
  282. // Move the checkbox inside the label
  283. fileLabel.insertBefore(fileCheckbox, fileLabel.firstChild);
  284. }
  285. }
  286.  
  287. // Modify the "Urgent report" label and checkbox
  288. const urgentCheckbox = form.querySelector('#urgent-checkbox');
  289. if (urgentCheckbox) {
  290. const urgentLabel = form.querySelector('label[for="urgent-checkbox"]');
  291. if (urgentLabel) {
  292. urgentLabel.textContent = 'Urgent'; // Modify the label text
  293. // Move the checkbox inside the label
  294. urgentLabel.parentNode.insertBefore(urgentCheckbox, urgentLabel);
  295. }
  296. }
  297.  
  298. // Set Reason input width equal to Password input width
  299. const passwordInput = form.querySelector('input[type="password"][name="password"]');
  300. const reasonInput = form.querySelector('input[type="text"][name="reason"]');
  301. if (passwordInput && reasonInput) {
  302. // Set the Reason input size equal to Password input's size
  303. reasonInput.setAttribute('size', passwordInput.getAttribute('size'));
  304. }
  305.  
  306. // Mark the form as modified using a dataset attribute to prevent re-modification
  307. form.dataset.modified = 'true';
  308. }
  309.  
  310. // Function to observe changes to the page (to handle dynamic form rendering)
  311. function observeFormChanges() {
  312. // Find all forms on the page that are for reporting
  313. const forms = document.querySelectorAll('form.post-actions');
  314.  
  315. forms.forEach(form => {
  316. modifyReportForm(form); // Apply modification to the form
  317. });
  318. }
  319.  
  320. // Initial run when the page loads
  321. window.addEventListener('load', function() {
  322. // Apply changes to any forms already present on the page
  323. observeFormChanges();
  324.  
  325. // Set up a MutationObserver to watch for changes in the DOM
  326. const observer = new MutationObserver((mutationsList) => {
  327. for (let mutation of mutationsList) {
  328. if (mutation.type === 'childList') {
  329. // Check for changes to form elements
  330. observeFormChanges();
  331. }
  332. }
  333. });
  334.  
  335. // Observe the body of the page for changes
  336. observer.observe(document.body, {
  337. childList: true,
  338. subtree: true
  339. });
  340. });
  341.  
  342. // Listen for any changes to checkboxes to ensure modifications are reapplied dynamically
  343. document.addEventListener('change', function(event) {
  344. if (event.target.classList.contains('delete')) {
  345. // When the checkbox is clicked, check for changes in the form
  346. const form = event.target.closest('form.post-actions');
  347. if (form) {
  348. modifyReportForm(form);
  349. }
  350. }
  351. });
  352.  
  353. // Custom CSS for .deadlink class
  354. const style = `<style>
  355. .deadlink {
  356. text-decoration: line-through !important;
  357. color: #789922;
  358. }
  359. </style>`;
  360. document.head.innerHTML += style;
  361.  
  362. // Modify quote links to be styled as dead links
  363. const quote = document.querySelectorAll('.quote');
  364.  
  365. quote.forEach(elem => {
  366. if (/^&gt;&gt;[1-9]+[0-9]*$/.test(elem.innerHTML)) {
  367. const postNum = elem.innerHTML;
  368. elem.outerHTML = `<span class="deadlink">${postNum}</span>`;
  369. }
  370. });
  371.  
  372. // Fix post times (relative and exact)
  373. function fixTime() {
  374. document.querySelectorAll(".post").forEach(e => {
  375. let timeElement = e.querySelector("[datetime]");
  376. let time = new Date(Date.parse(timeElement.getAttribute("datetime")));
  377. 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)}`;
  378. let relativeTime;
  379. let difference = (Date.now() - time.getTime()) / 1000;
  380.  
  381. if (difference < 10) relativeTime = "Just now";
  382. else if (difference < 60) relativeTime = `${Math.floor(difference)} seconds ago`;
  383. else if (difference < 120) relativeTime = `1 minute ago`;
  384. else if (difference < 3600) relativeTime = `${Math.floor(difference / 60)} minutes ago`;
  385. else if (difference < 7200) relativeTime = `1 hour ago`;
  386. else if (difference < 86400) relativeTime = `${Math.floor(difference / 3600)} hours ago`;
  387. else if (difference < 172800) relativeTime = `1 day ago`;
  388. else if (difference < 2678400) relativeTime = `${Math.floor(difference / 86400)} days ago`;
  389. else if (difference < 5356800) relativeTime = `1 month ago`;
  390. else if (difference < 31536000) relativeTime = `${Math.floor(difference / 2678400)} months ago`;
  391. else if (difference < 63072000) relativeTime = `1 year ago`;
  392. else relativeTime = `${Math.floor(difference / 31536000)} years ago`;
  393.  
  394. if (isEnabled("force-exact-time")) {
  395. timeElement.innerText = exactTime;
  396. timeElement.setAttribute("title", relativeTime);
  397. } else {
  398. timeElement.innerText = relativeTime;
  399. timeElement.setAttribute("title", exactTime);
  400. }
  401. });
  402. }
  403.  
  404. // Initialize post fixes
  405. function initFixes() {
  406. // Add formatting help to comment section
  407. document.querySelectorAll("form[name=post] th").forEach(e => {
  408. if (e.innerText === "Comment") {
  409. 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>`);
  410. }
  411. });
  412.  
  413. // Add the formatting help next to "Email" column header with a new class
  414. document.querySelectorAll("form[name=post] th").forEach(e => {
  415. if (e.innerText === "Email") {
  416. // Insert the formatting help button next to the "Email" header with a new class
  417. e.insertAdjacentHTML("beforeend", `<sup title="Formatting help for Email" class="sf-formatting-help-email">?</sup>`);
  418. }
  419. });
  420.  
  421. // Handle the click event for the new "sf-formatting-help-email" button
  422. document.addEventListener('click', function(event) {
  423. let t = event.target;
  424.  
  425. // Check if the clicked element is the "sf-formatting-help-email" button (for "Email")
  426. if (t.classList.contains("sf-formatting-help-email")) {
  427. // Trigger the site's existing popup system (if it exists, e.g., customAlert)
  428. customAlert(`
  429. <h1>Email Field</h1>
  430. <p>bump</p>
  431. <p>sage</p>
  432. <p>supersage</p>
  433. <p>anonymous</p>
  434. <p>anonymous sage</p>
  435. <p>flag</p>
  436. <p>flag sage</p>
  437. `); // Show the custom message with multiple lines inside the existing popup
  438. }
  439. });
  440.  
  441. // 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.)
  442. if (typeof GM_xmlhttpRequest === "function") {
  443. let fileSelectionInterval = setInterval(() => {
  444. if (select = document.querySelector("#upload_selection")) {
  445. select.childNodes[0].insertAdjacentHTML('afterend', ` / <a href="javascript:void(0)" id="sf-file-url"></a>`);
  446. clearInterval(fileSelectionInterval);
  447. }
  448. }, 100);
  449. }
  450.  
  451. // Handle dynamic updates
  452. document.addEventListener("dyn_update", e => {
  453. e.detail.forEach(e => fixPost(e));
  454. fixTime();
  455. addExpandos();
  456. });
  457.  
  458. // Handle post updates
  459. document.addEventListener("post_update", e => {
  460. e.detail.forEach(node => {
  461. if (node.addedNodes[0].nodeName === "DIV") {
  462. fixPost(node.addedNodes[0]);
  463. }
  464. });
  465. fixTime();
  466. addExpandos();
  467. });
  468.  
  469. // Apply fixes to existing posts
  470. [...document.getElementsByClassName("post")].forEach(e => {
  471. fixPost(e);
  472. });
  473. fixTime();
  474. addExpandos();
  475. }
  476.  
  477. // DONT REMOVE THIS PART, IT WILL BREAK THE SCRIPT
  478. // undo filter
  479. function undoFilter(post) {
  480. // if (isEnabled("restore-filtered")) {
  481. // post.querySelectorAll(".body, .body *, .replies, .replies *").forEach(e => {
  482. // e.childNodes.forEach(e => {
  483. // if (e.nodeName == "#text") {
  484. // e.nodeValue = e.nodeValue.replaceAll("im trans btw", "kuz");
  485. // }
  486. // });
  487. // });
  488. // }
  489. }
  490.  
  491.  
  492. // Catalog Fixes: Adjust thread timestamps & undo filters
  493. document.querySelectorAll("#Grid > div").forEach(e => {
  494. let threadTime = new Date(parseInt(e.getAttribute("data-time")) * 1000);
  495. e.getElementsByClassName("thread-image")[0].setAttribute("title", `${months[threadTime.getMonth()]} ${("0" + threadTime.getDate()).slice(-2)}` +
  496. ` ${("0" + threadTime.getHours()).slice(-2)}:${("0" + threadTime.getMinutes()).slice(-2)}`);
  497. undoFilter(e);
  498. });
  499.  
  500. // Keyboard Shortcuts
  501. window.addEventListener("keydown", e => {
  502. if (e.key == "Enter" && (e.ctrlKey || e.metaKey)) {
  503. if (form = e.target.closest("form[name=post]")) {
  504. form.querySelector("input[type=submit]").click();
  505. }
  506. }
  507. });
  508.  
  509. document.getElementById("sort_by").addEventListener("change", function() {
  510. const selectedValue = this.value;
  511. let threads = Array.from(document.querySelectorAll(".catty-thread"));
  512.  
  513. // Separate stickied threads
  514. const stickiedThreads = threads.filter(thread => thread.dataset.sticky === "true");
  515. const regularThreads = threads.filter(thread => thread.dataset.sticky === "false");
  516.  
  517. // Function to sort regular threads
  518. const sortThreads = (threads, criteria) => {
  519. switch(criteria) {
  520. case "bump:desc":
  521. return threads.sort((a, b) => parseInt(b.getAttribute("data-bump")) - parseInt(a.getAttribute("data-bump")));
  522. case "time:desc":
  523. return threads.sort((a, b) => parseInt(b.getAttribute("data-time")) - parseInt(a.getAttribute("data-time")));
  524. case "reply:desc":
  525. return threads.sort((a, b) => parseInt(b.getAttribute("data-reply")) - parseInt(a.getAttribute("data-reply")));
  526. case "random:desc":
  527. return threads.sort(() => Math.random() - 0.5);
  528. default:
  529. return threads;
  530. }
  531. };
  532.  
  533. // Sort regular threads
  534. const sortedRegularThreads = sortThreads(regularThreads, selectedValue);
  535.  
  536. // Clear current displayed threads
  537. const grid = document.getElementById("Grid");
  538. grid.innerHTML = ""; // Clear current threads
  539.  
  540. // Append stickied threads first
  541. stickiedThreads.forEach(thread => {
  542. grid.appendChild(thread);
  543. });
  544.  
  545. // Append sorted regular threads
  546. sortedRegularThreads.forEach(thread => {
  547. grid.appendChild(thread);
  548. });
  549. });
  550.  
  551. // Autofocus Textarea on Certain Pages
  552. if ((textarea = document.querySelector("textarea[name=body]")) && document.documentElement.classList.contains("desktop-style") && window.location.hash[1] != "q") {
  553. textarea.focus({
  554. preventScroll: true
  555. });
  556. }
  557.  
  558. // --- Character Counter Feature ---
  559. const observer = new MutationObserver(() => {
  560. const commentHeader = Array.from(document.querySelectorAll('th')).find(el => el.textContent.includes('Comment'));
  561.  
  562. if (commentHeader) {
  563. // Stop observing once the Comment header is found
  564. observer.disconnect();
  565.  
  566. // Create the character counter container under the "Comment" heading
  567. const counterContainer = document.createElement('div');
  568. counterContainer.style.marginTop = '10px';
  569.  
  570. const counterText = document.createElement('span');
  571. counterText.setAttribute('id', 'char-count');
  572. counterText.textContent = '0 / 24000';
  573.  
  574. counterContainer.appendChild(counterText);
  575. commentHeader.appendChild(counterContainer);
  576.  
  577. // Find the textarea for the comment
  578. const textArea = document.querySelector('textarea[name="body"]');
  579. if (textArea) {
  580. // Update character count as the user types
  581. textArea.addEventListener('input', function() {
  582. const currentLength = textArea.value.length;
  583. const maxLength = 24000;
  584.  
  585. // Update the counter
  586. counterText.textContent = `${currentLength} / ${maxLength}`;
  587. });
  588. }
  589. }
  590. });
  591.  
  592. // Start observing the DOM for changes in the body element
  593. observer.observe(document.body, {
  594. childList: true,
  595. subtree: true
  596. });
  597.  
  598. // Password Box Toggle (Show/Hide Password)
  599. if (passwordBox = document.querySelector("form[name=post] input[name=password]")) {
  600. passwordBox.setAttribute("type", "password");
  601. passwordBox.insertAdjacentHTML("afterend", `<input type="button" name="toggle-password" value="Show">`);
  602. }
  603.  
  604.  
  605. // Form Submit Cooldown
  606. document.querySelectorAll("form[name=post] input[type=submit]").forEach(e => {
  607. e.setAttribute("og-value", e.getAttribute("value"));
  608. });
  609.  
  610. setInterval(() => {
  611. let lastTime = getNumber("lastTime");
  612. let difference = 11 - Math.ceil((Date.now() - lastTime) / 1000);
  613. let buttons = document.querySelectorAll("form[name=post] input[type=submit]");
  614.  
  615. if ([...buttons].find(e => e.value.includes("Post"))) {
  616. return;
  617. } else if (difference > 0) {
  618. let disableButton = isEnabled("disable-submit-on-cooldown");
  619. buttons.forEach(e => {
  620. e.value = `${e.getAttribute("og-value")} (${difference})`;
  621. if (disableButton) {
  622. e.setAttribute("disabled", "disabled");
  623. }
  624. });
  625. } else {
  626. buttons.forEach(e => {
  627. e.value = e.getAttribute("og-value");
  628. e.removeAttribute("disabled");
  629. });
  630. }
  631. }, 100);
  632.  
  633.  
  634. // Thread Hiding Logic
  635. function areHiddenThreads() {
  636. let threadGrid = document.getElementById("Grid");
  637. if (document.querySelector(".catty-thread.hidden")) {
  638. if (!document.getElementById("toggle-hidden")) {
  639. document.querySelector(".desktop-style #image_size, .mobile-style header").insertAdjacentHTML("afterend", `<span id="toggle-hidden"></span>`);
  640. }
  641. } else if (toggleButton = document.getElementById("toggle-hidden")) {
  642. toggleButton.remove();
  643. document.body.classList.remove("showing-hidden");
  644. }
  645. }
  646.  
  647.  
  648. // Catalog Navigation and Search Form
  649. if (document.body.classList.contains("active-catalog")) {
  650. if (isEnabled("catalog-navigation")) {
  651. document.querySelectorAll(".boardlist a[href*='index.html']").forEach(e => e.href = e.href.replace("index.html", "catalog.html"));
  652. }
  653.  
  654. document.querySelector("#image_size").insertAdjacentHTML("afterend", `
  655. <form style="display: inline-block; margin-bottom: 0px; height: 10px;" action="/search.php">
  656. <p>
  657. <input type="text" name="search" placeholder="${board} search">
  658. <input type="hidden" name="board" value="${board}">
  659. <input type="submit" value="Search">
  660. </p>
  661. </form>
  662. `);
  663.  
  664. let hiddenThreads = getJson("hiddenthreads");
  665. let hasThreads = hiddenThreads.hasOwnProperty(board);
  666.  
  667. document.querySelectorAll(".mix").forEach(e => {
  668. e.classList.replace("mix", "catty-thread");
  669. if (hasThreads && hiddenThreads[board].hasOwnProperty(e.getAttribute("data-id"))) {
  670. e.classList.add("hidden");
  671. delete hiddenThreads[board][e.getAttribute("data-id")];
  672. }
  673. if (e.getAttribute("data-sticky") == "true") {
  674. e.parentNode.prepend(e);
  675. }
  676. });
  677.  
  678. if (hasThreads) {
  679. Object.keys(hiddenThreads[board]).forEach(e => {
  680. removeFromJson("hiddenthreads", `${board}.${e}`);
  681. });
  682. }
  683.  
  684. areHiddenThreads();
  685. }
  686.  
  687.  
  688. // Event Listeners for Clicks and Inputs
  689. document.addEventListener("click", e => {
  690. let t = e.target;
  691.  
  692. // Submit button in post form
  693. if (t.matches("form[name=post] input[type=submit]")) {
  694. t.value = t.getAttribute("og-value");
  695.  
  696. // Bypass filter and modify text
  697. if (isEnabled("bypass-filter")) {
  698. let textbox = t.closest("tbody").querySelector("textarea[name=body]");
  699. textbox.value = textbox.value.replaceAll(/(discord)/ig, str => {
  700. let arr = [];
  701. while (!arr.includes("​")) {
  702. arr = [];
  703. [...str].forEach((c, i) => {
  704. if (Math.random() < 0.5 && i != 0) {
  705. arr.push("​");
  706. }
  707. arr.push(c);
  708. if (Math.random() > 0.5 && i != str.length - 1) {
  709. arr.push("​");
  710. }
  711. });
  712. }
  713. return arr.join("");
  714. });
  715. }
  716. }
  717. // Toggle password visibility
  718. else if (t.matches("input[name=toggle-password]")) {
  719. if (passwordBox.getAttribute("type") == "password") {
  720. passwordBox.setAttribute("type", "text");
  721. t.value = "Hide";
  722. } else {
  723. passwordBox.setAttribute("type", "password");
  724. t.value = "Show";
  725. }
  726. }
  727.  
  728. // Mass reply functionality
  729. else if (t.id == "mass-reply") {
  730. let massReply = "";
  731. document.querySelectorAll("[href*='#q']").forEach(e => {
  732. massReply += `>>${e.textContent}\n`;
  733. });
  734. document.querySelectorAll("textarea[name=body]").forEach(e => {
  735. e.value += massReply;
  736. e.focus();
  737. });
  738. }
  739.  
  740. // Mass quote or mass orange functionality
  741. else if (t.id == "mass-quote" || t.id == "mass-orange") {
  742. document.body.classList.add("hide-quote-buttons");
  743. let selection = window.getSelection();
  744. let range = document.createRange();
  745. range.selectNodeContents(document.body);
  746. selection.removeAllRanges();
  747. selection.addRange(range);
  748.  
  749. let massQuote = window.getSelection().toString().replace(/(\r\n|\r|\n|^)/g, t.id == "mass-quote" ? "$1>" : "$1<") + "\n";
  750. selection.removeAllRanges();
  751. document.body.classList.remove("hide-quote-buttons");
  752.  
  753. document.querySelectorAll("textarea[name=body]").forEach(e => {
  754. e.value += massQuote;
  755. e.focus();
  756. });
  757. }
  758.  
  759. // Quick quote or quick orange functionality
  760. else if (t.classList.contains("quick-quote") || t.classList.contains("quick-orange")) {
  761. let quote = t.closest(".post").querySelector(".body").innerText.replace(/(\r\n|\r|\n|^)/g, t.classList.contains("quick-quote") ? "$1>" : "$1<") + "\n";
  762. document.querySelectorAll("textarea[name=body]").forEach(e => {
  763. e.value += quote;
  764. e.focus();
  765. });
  766. }
  767.  
  768. // Comment quote or comment orange functionality
  769. else if (t.classList.contains("comment-quote") || t.classList.contains("comment-orange")) {
  770. document.querySelectorAll("textarea[name=body]").forEach(e => {
  771. e.value = e.value.replace(/(\r\n|\r|\n|^)/g, t.classList.contains("comment-quote") ? "$1>" : "$1<");
  772. e.focus();
  773. });
  774. }
  775.  
  776. // Toggle visibility of threads
  777. else if ((e.shiftKey || (e.detail == 3 && (document.documentElement.matches(".mobile-style") || isEnabled("desktop-triple-click")))) &&
  778. t.matches(".active-catalog .catty-thread *, .active-catalog .catty-thread")) {
  779. e.preventDefault();
  780. let thread = t.closest(".catty-thread");
  781. thread.classList.toggle("hidden");
  782.  
  783. if (thread.classList.contains("hidden")) {
  784. addToJson("hiddenthreads", `${board}.${thread.getAttribute("data-id")}`, Math.floor(Date.now() / 1000));
  785. } else {
  786. removeFromJson("hiddenthreads", `${board}.${thread.getAttribute("data-id")}`);
  787. }
  788. areHiddenThreads();
  789. }
  790.  
  791. // Toggle hidden threads visibility
  792. else if (t.id == "toggle-hidden") {
  793. document.body.classList.toggle("showing-hidden");
  794. }
  795.  
  796. // Hide/unhide thread link functionality
  797. else if (t.classList.contains("hide-thread-link") || t.classList.contains("unhide-thread-link")) {
  798. setValue("hiddenthreads", localStorage.getItem("hiddenthreads"));
  799. }
  800.  
  801. // Hide blotter functionality
  802. else if (t.classList.contains("hide-blotter")) {
  803. setValue("hidden-blotter", document.querySelector(".blotter").innerText);
  804. document.body.classList.add("hidden-blotter");
  805. }
  806.  
  807. // SF-expander button functionality (manual cutoff)
  808. else if (t.classList.contains("sf-expander")) {
  809. t.closest(".post").setAttribute("manual-cutoff", t.closest(".post").classList.toggle("sf-cutoff"));
  810. }
  811.  
  812. // Formatting help popup
  813. else if (t.classList.contains("sf-formatting-help")) {
  814. let help = `
  815. <h1>Comment Field</h1>
  816. <span class="heading">Font Guide</span><br>
  817. <span class="spoiler">**Spoiler**</span><br>
  818. <em>''Italics''</em><br>
  819. <b>'''Bold'''</b><br>
  820. <code>\`\`\`Codetext\`\`\`</code><br>
  821. <u>__Underline__</u><br>
  822. <s>~~Strikethrough~~</s><br>
  823. <big>+=Bigtext=+</big><br>
  824. <span class="rotate">##Spintext##</span><small><sup> (disabled)</sup></small><br>
  825. <span class="quote">&gt;Greentext</span><br>
  826. <span class="quote2">&lt;Orangetext</span><br>
  827. <span class="quote3" style="color: #6577E6;">^Bluetext</span><br>
  828. <span class="heading">==Redtext==</span><br>
  829. <span class="heading2">--Bluetext--</span><br>
  830. <font color="FD3D98"><b>-~-Pinktext-~-</b></font><br>
  831. <span class="glow">%%Glowtext%%</span><br>
  832. <span style="text-shadow:0px 0px 40px #36d7f7, 0px 0px 2px #36d7f7">;;Blueglowtext;;</span><br>
  833. <span style="text-shadow:0px 0px 40px #fffb00, 0px 0px 2px #fffb00">::Yellowglowtext::</span><br>
  834. <span style="text-shadow:0px 0px 40px #ff0000, 0px 0px 2px #ff0000">!!Redglowtext!!</span><small><sup> (put after each word)</sup></small><br>
  835. <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>
  836. <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>
  837. <span style="background:#faf8f8;color:#3060a8">(((Zionisttext)))</span><br>
  838. <br>
  839. <span class="heading">Linking</span><br>
  840. Link to a post on the current board<br>
  841. >>1234<br>
  842. Link to another board<br>
  843. >>>/soy/<br>
  844. Link to a post on another board<br>
  845. >>>/soy/1234<br>
  846. <br>
  847. <span class="heading">Wordfilters</span><br>
  848. See <a href="https://wiki.soyjak.st/Wordfilter" target="_blank">https://wiki.soyjak.st/Wordfilter</a><br>
  849. `;
  850. customAlert(help);
  851. }
  852. });
  853.  
  854. // Search Input Filter for Catalog Threads
  855. document.addEventListener("input", e => {
  856. let t = e.target;
  857. if (t.matches("input[name=search]") && document.querySelector(".sf-catty, .active-catalog")) {
  858. document.querySelectorAll(".catty-thread").forEach(e => {
  859. if (e.innerText.toLowerCase().includes(t.value.toLowerCase())) {
  860. e.classList.remove("sf-filtered");
  861. } else {
  862. e.classList.add("sf-filtered");
  863. }
  864. });
  865. }
  866. });
  867.  
  868.  
  869. // Blotter Hide/Show Button
  870. if (blotter = document.querySelector(".blotter")) {
  871. blotter.insertAdjacentHTML("beforebegin", `<a class="hide-blotter" href="javascript:void(0)">[–]</a>`);
  872. if (blotter.innerText == getValue("hidden-blotter") || isEnabled("hide-blotter")) {
  873. document.body.classList.add("hidden-blotter");
  874. }
  875. }
  876.  
  877. document.head.insertAdjacentHTML("beforeend", `
  878. <style>
  879. /* Hide elements in specific conditions */
  880. .hide-blotter {
  881. float: left;
  882. }
  883.  
  884. .hidden-blotter .blotter,
  885. .hidden-blotter .blotter + hr,
  886. .hidden-blotter .hide-blotter,
  887. .catty-thread.hidden,
  888. .showing-hidden .catty-thread,
  889. .mobile-style .g-recaptcha-bubble-arrow,
  890. .catty-thread.sf-filtered,
  891. .showing-hidden .catty-thread.hidden.sf-filtered,
  892. .hide-quote-buttons .quote-buttons {
  893. display: none !important;
  894. }
  895.  
  896. /* Styling for expander button in replies */
  897. .reply .sf-expander {
  898. margin-left: 1.8em;
  899. padding-right: 3em;
  900. padding-bottom: 0.3em;
  901. }
  902.  
  903. .sf-expander::after {
  904. content: "[Hide Full Text]";
  905. }
  906.  
  907. .sf-cutoff .sf-expander::after {
  908. content: "[Show Full Text]";
  909. }
  910.  
  911. /* File URL input styling */
  912. #sf-file-url::after {
  913. content: "URL";
  914. }
  915.  
  916. #sf-file-url.sf-loading::after {
  917. content: "Loading...";
  918. }
  919.  
  920. /* Hover styling for sharty */
  921. #sharty-hover {
  922. pointer-events: none;
  923. position: fixed;
  924. z-index: 500;
  925. }
  926.  
  927. /* Display adjustments for threads */
  928. .catty-thread,
  929. .showing-hidden .catty-thread.hidden {
  930. display: inline-block !important;
  931. }
  932.  
  933. /* Toggle hidden threads button styling */
  934. #toggle-hidden {
  935. text-decoration: underline;
  936. color: #34345C;
  937. cursor: pointer;
  938. user-select: none;
  939. }
  940.  
  941. #toggle-hidden::before {
  942. content: "[Show Hidden]";
  943. }
  944.  
  945. .showing-hidden #toggle-hidden::before {
  946. content: "[Hide Hidden]";
  947. }
  948.  
  949. #image_size + #toggle-hidden {
  950. display: inline-block;
  951. padding-left: 5px;
  952. }
  953.  
  954. header + #toggle-hidden {
  955. display: block;
  956. margin: 1em auto;
  957. width: fit-content;
  958. }
  959.  
  960. /* Recaptcha bubble styling on mobile */
  961. .mobile-style .g-recaptcha-bubble-arrow + div {
  962. position: fixed !important;
  963. left: 50%;
  964. top: 50%;
  965. transform: translate(-50%, -50%);
  966. -webkit-transform: translate(-50%, -50%);
  967. }
  968.  
  969. /* Password input field adjustments */
  970. input[name=toggle-password] {
  971. margin-left: 2px;
  972. }
  973.  
  974. /* Formatting help link styling */
  975. .sf-formatting-help {
  976. cursor: pointer;
  977. }
  978.  
  979. /* Formatting email link styling */
  980. .sf-formatting-help-email {
  981. cursor: pointer;
  982. }
  983.  
  984. /* Comment quote button styling */
  985. .comment-quotes {
  986. text-align: center;
  987. }
  988.  
  989. /* Truncate long posts styling (conditionally enabled) */
  990. ${isEnabled("truncate-long-posts") ? `
  991. .sf-cutoff:not(.post-hover) .body {
  992. overflow: hidden;
  993. word-break: break-all;
  994. display: -webkit-box;
  995. min-width: min-content;
  996. -webkit-line-clamp: 20;
  997. -webkit-box-orient: vertical;
  998. }
  999.  
  1000. .sf-cutoff.post-hover .sf-expander {
  1001. display: none !important;
  1002. }
  1003.  
  1004. div.post.reply.sf-cutoff div.body {
  1005. margin-bottom: 0.3em;
  1006. padding-bottom: unset;
  1007. }
  1008. ` : ""}
  1009. </style>
  1010. `);
  1011.  
  1012. // Initialize additional fixes
  1013. initFixes();