Sharty fixes

Enhancements for the 'ty

  1. // ==UserScript==
  2. // @name Sharty fixes
  3. // @namespace soyjak.party
  4. // @match http*://soyjak.party/*
  5. // @match http*://www.soyjak.party/*
  6. // @grant GM_getValue
  7. // @grant GM_setValue
  8. // @grant GM_xmlhttpRequest
  9. // @connect *
  10. // @version 2.16.2
  11. // @author Xyl
  12. // @description Enhancements for the 'ty
  13. // ==/UserScript==
  14.  
  15. const version = "v2.16.2";
  16. console.log(`Sharty fixes ${version}`);
  17.  
  18. const namespace = "ShartyFixes.";
  19. function setValue(key, value) {
  20. if (key == "hiddenthreads" || key == "hiddenimages") {
  21. if (typeof GM_setValue == "function") {
  22. GM_setValue(key, value);
  23. }
  24. localStorage.setItem(key, value);
  25. } else {
  26. if (typeof GM_setValue == "function") {
  27. GM_setValue(namespace + key, value);
  28. } else {
  29. localStorage.setItem(namespace + key, value);
  30. }
  31. }
  32. }
  33.  
  34. function getValue(key) {
  35. if (key == "hiddenthreads" || key == "hiddenimages") {
  36. if (typeof GM_getValue == "function" && GM_getValue(key)) {
  37. localStorage.setItem(key, GM_getValue(key).toString());
  38. }
  39. return localStorage.getItem(key);
  40. }
  41. if (typeof GM_getValue == "function") {
  42. return GM_getValue(namespace + key);
  43. } else {
  44. return localStorage.getItem(namespace + key);
  45. }
  46. }
  47.  
  48. function isEnabled(key) {
  49. let value = getValue(key);
  50. if (value == null) {
  51. value = optionsEntries[key][2];
  52. setValue(key, value);
  53. }
  54. return value.toString() == "true";
  55. }
  56.  
  57. function getNumber(key) {
  58. let value = parseInt(getValue(key));
  59. if (Number.isNaN(value)) {
  60. value = 0;
  61. }
  62. return value;
  63. }
  64.  
  65. function getJson(key) {
  66. let value = getValue(key);
  67. if (value == null) {
  68. value = "{}";
  69. }
  70. return JSON.parse(value);
  71. }
  72.  
  73. function addToJson(key, jsonKey, value) {
  74. let json = getJson(key);
  75. let parent = json;
  76. jsonKey.split(".").forEach((e, index, array) => {
  77. if (index < array.length - 1) {
  78. if (!parent.hasOwnProperty(e)) {
  79. parent[e] = {};
  80. }
  81. parent = parent[e];
  82. } else {
  83. parent[e] = value;
  84. }
  85. });
  86. setValue(key, JSON.stringify(json));
  87. return json;
  88. }
  89.  
  90. function removeFromJson(key, jsonKey) {
  91. let json = getJson(key);
  92. let parent = json;
  93. jsonKey.split(".").forEach((e, index, array) => {
  94. if (index < array.length - 1) {
  95. parent = parent[e];
  96. } else {
  97. delete parent[e];
  98. }
  99. });
  100. setValue(key, JSON.stringify(json));
  101. return json;
  102. }
  103.  
  104. function customAlert(a) {
  105. document.body.insertAdjacentHTML("beforeend", `
  106. <div id="alert_handler">
  107. <div id="alert_background" onclick="this.parentNode.remove()"></div>
  108. <div id="alert_div">
  109. <a id='alert_close' href="javascript:void(0)" onclick="this.parentNode.parentNode.remove()"><i class='fa fa-times'></i></a>
  110. <div id="alert_message">${a}</div>
  111. <button class="button alert_button" onclick="this.parentNode.parentNode.remove()">OK</button>
  112. </div>
  113. </div>`);
  114. }
  115.  
  116. const fileToMime = {
  117. "jpg": "image/jpeg",
  118. "jpeg": "image/jpeg",
  119. "jfif": "image/jpeg",
  120. "png": "image/png",
  121. "gif": "image/gif",
  122. "avif": "image/avif",
  123. "bmp": "image/bmp",
  124. "tif": "image/tiff",
  125. "tiff": "image/tiff",
  126. "webp": "image/webp",
  127. "aac": "audio/aac",
  128. "flac": "audio/flac",
  129. "mid": "audio/midi",
  130. "midi": "audio/midi",
  131. "mp3": "audio/mpeg",
  132. "ogg": "audio/ogg",
  133. "opus": "audio/opus",
  134. "wav": "audio/wav",
  135. "weba": "audio/webm",
  136. "mp4": "video/mp4",
  137. "webm": "video/webm",
  138. "pdf": "application/pdf"
  139. }
  140.  
  141. const optionsEntries = {
  142. "autofill-captcha": ["checkbox", "Autofill text captcha when in use (recommended, only disable if causing issues)", true],
  143. "bypass-filter": ["checkbox", "Try to bypass word filter when posting", true],
  144. // "restore-filtered": ["checkbox", "Try to restore filtered words", false],
  145. "show-quote-button": ["checkbox", "Show quick quote button", true],
  146. "mass-reply-quote": ["checkbox", "Enable mass reply and mass quote buttons", false],
  147. "anonymise": ["checkbox", "Anonymise name and tripfags", false],
  148. "hover-images": ["checkbox", "Popup image on hover", true],
  149. "hover-on-catalog": ["checkbox", "Show image hover on the catalog", true],
  150. "hide-blotter": ["checkbox", "Always hide blotter", false],
  151. "truncate-long-posts": ["checkbox", "Truncate line spam", false],
  152. "disable-submit-on-cooldown": ["checkbox", "Disable submit button on cooldown", false],
  153. "desktop-triple-click": ["checkbox", "Enable triple click to hide catalog thread on desktop", false],
  154. "force-exact-time": ["checkbox", "Show exact time", false],
  155. "hide-sage-images": ["checkbox", "Hide sage images by default (help mitigate gross spam)", false],
  156. "catalog-navigation": ["checkbox", "Board list links to catalogs when on catalog", true]
  157. }
  158. let options = Options.add_tab("sharty-fixes", "gear", "Sharty Fixes").content[0];
  159. let optionsHTML = `<span style="display: block; text-align: center">${version}</span>`;
  160. optionsHTML += `<a style="display: block; text-align: center" href="https://booru.soy/post/list/variant%3Acobson/1">#cobgang</a><br>`;
  161. for ([optKey, optValue] of Object.entries(optionsEntries)) {
  162. optionsHTML += `<input type="${optValue[0]}" id="${optKey}" name="${optKey}"><label for="${optKey}">${optValue[1]}</label><br>`;
  163. }
  164. options.insertAdjacentHTML("beforeend", optionsHTML);
  165.  
  166. options.querySelectorAll("input[type=checkbox]").forEach(e => {
  167. e.checked = isEnabled(e.id);
  168. e.addEventListener("change", e => {
  169. setValue(e.target.id, e.target.checked);
  170. });
  171. });
  172.  
  173. // redirect
  174. if (location.origin.match(/(http:|\/www)/g)) {
  175. location.replace(`https://soyjak.party${location.pathname}${location.hash}`);
  176. }
  177.  
  178. const board = window.location.pathname.split("/")[1];
  179.  
  180. // post fixes
  181. const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
  182. const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  183. if (document.body.classList.contains("active-thread")) {
  184. const updateObserver = new MutationObserver(list => {
  185. const evt = new CustomEvent("post_update", {
  186. detail: list
  187. });
  188. document.dispatchEvent(evt);
  189. });
  190. updateObserver.observe(document.querySelector(".thread"), {
  191. childList: true
  192. });
  193. }
  194.  
  195. let intervals = {};
  196. function fixPost(post) {
  197. let timeElement = post.querySelector("[datetime]");
  198. let time = new Date(Date.parse(timeElement.getAttribute("datetime")));
  199. let isOwnPost;
  200. let postNumber = post.getElementsByClassName("post_no")[1];
  201. let postText = postNumber.textContent;
  202.  
  203. if (email = post.querySelector("a.email")) {
  204. if (isEnabled("hide-sage-images") && !post.classList.contains("image-hide-processed") && email.href.match(/mailto:sage$/i)) {
  205. let localStorageBackup = localStorage.getItem("hiddenimages");
  206. let interval = setInterval(() => {
  207. if (document.querySelector(`[id*="${postText}"] .hide-image-link`)) {
  208. post.classList.add("image-hide-processed");
  209. clearInterval(interval);
  210. document.querySelector(`[id*="${postText}"] .files`).querySelectorAll(".hide-image-link:not([style*='none'])").forEach(e => e.click());
  211. localStorage.setItem("hiddenimages", localStorageBackup);
  212. }
  213. }, 50);
  214. }
  215. }
  216. try { isOwnPost = JSON.parse(localStorage.getItem("own_posts"))[board].includes(post.querySelector(".post_no[onclick*=cite]").innerText) } catch { isOwnPost = false };
  217. if (isOwnPost && getNumber("lastTime") < time.getTime()) {
  218. setValue("lastTime", time.getTime());
  219. }
  220. timeElement.outerHTML = `<span datetime=${timeElement.getAttribute("datetime")}>${timeElement.innerText}</span>`;
  221. post.querySelector(".intro").insertAdjacentHTML("beforeend", `<span class="quote-buttons"></span>`);
  222. if (isEnabled("show-quote-button")) {
  223. post.querySelector(".quote-buttons").insertAdjacentHTML("beforeend", `<a href="javascript:void(0);" class="quick-quote">[>]</a>`);
  224. post.querySelector(".quote-buttons").insertAdjacentHTML("beforeend", `<a href="javascript:void(0);" class="quick-orange">[<]</a>`);
  225. }
  226. if (isEnabled("mass-reply-quote") && post.classList.contains("op") && post.closest(".active-thread")) {
  227. 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>`);
  228. }
  229. let body = post.querySelector(".body");
  230. body.childNodes.forEach(e => {
  231. if (e.nodeType == 3) {
  232. let span = document.createElement("span");
  233. span.innerText = e.textContent;
  234. e.parentNode.replaceChild(span, e);
  235. }
  236. });
  237. if (document.body.classList.contains("active-thread")) {
  238. postNumber.href = `#q${postNumber.textContent}`;
  239. postNumber.setAttribute("onclick", `$(window).trigger('cite', [${postNumber.textContent, null}]);`);
  240. postNumber.addEventListener("click", () => {
  241. let selection = window.getSelection().toString();
  242. document.querySelectorAll("textarea[name=body]").forEach(e => {
  243. e.value += `>>${postNumber.textContent}\n${selection != "" ? selection.replace(/(\r\n|\r|\n|^)/g, "$1>") : ""}`;
  244. });
  245. });
  246. }
  247. if (isEnabled("anonymise")) {
  248. post.querySelector(".name").textContent = "Chud";
  249. if (trip = post.querySelector(".trip")) {
  250. trip.remove();
  251. }
  252. }
  253. post.querySelectorAll("a").forEach(a => {
  254. a.href = decodeURIComponent(a.href.replace("https://jump.kolyma.net/?", ""));
  255. });
  256. undoFilter(post);
  257. }
  258.  
  259. function addExpandos() {
  260. if (isEnabled("truncate-long-posts")) {
  261. document.querySelectorAll(".post").forEach(e => {
  262. let body = e.querySelector(".body");
  263. e.classList.add("sf-cutoff");
  264. if (body.scrollHeight > body.offsetHeight) {
  265. if (!e.querySelector(".sf-expander")) {
  266. body.insertAdjacentHTML("afterend", `<br><a href="javascript:void(0)" class="sf-expander"></a>`);
  267. }
  268. if (e.getAttribute("manual-cutoff") == "false" || (window.location.hash.includes(e.id.split("_")[1]) && !e.getAttribute("manual-cutoff"))) {
  269. e.classList.remove("sf-cutoff");
  270. }
  271. } else if (body.scrollHeight == body.offsetHeight) {
  272. if (expander = e.querySelector(".sf-expander")) {
  273. expander.remove();
  274. }
  275. e.classList.remove("sf-cutoff");
  276. }
  277. });
  278. }
  279. }
  280.  
  281. window.addEventListener("resize", () => addExpandos());
  282.  
  283.  
  284. function fixTime() {
  285. document.querySelectorAll(".post").forEach(e => {
  286. let timeElement = e.querySelector("[datetime]");
  287. let time = new Date(Date.parse(timeElement.getAttribute("datetime")));
  288. 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)}`;
  289. let relativeTime;
  290. let difference = (Date.now() - time.getTime()) / 1000;
  291. if (difference < 10) {
  292. relativeTime = "Just now";
  293. } else if (difference < 60) {
  294. relativeTime = `${Math.floor(difference)} seconds ago`;
  295. } else if (difference < 120) {
  296. relativeTime = `1 minute ago`;
  297. } else if (difference < 3600) {
  298. relativeTime = `${Math.floor(difference/60)} minutes ago`;
  299. } else if (difference < 7200) {
  300. relativeTime = `1 hour ago`;
  301. } else if (difference < 86400) {
  302. relativeTime = `${Math.floor(difference/3600)} hours ago`;
  303. } else if (difference < 172800) {
  304. relativeTime = `1 day ago`;
  305. } else if (difference < 2678400) {
  306. relativeTime = `${Math.floor(difference/86400)} days ago`;
  307. } else if (difference < 5356800) {
  308. relativeTime = `1 month ago`;
  309. } else if (difference < 31536000) {
  310. relativeTime = `${Math.floor(difference/2678400)} months ago`;
  311. } else if (difference < 63072000) {
  312. relativeTime = `1 year ago`;
  313. } else {
  314. relativeTime = `${Math.floor(difference/31536000)} years ago`;
  315. }
  316. if (isEnabled("force-exact-time")) {
  317. timeElement.innerText = exactTime;
  318. timeElement.setAttribute("title", relativeTime);
  319. } else {
  320. timeElement.innerText = relativeTime;
  321. timeElement.setAttribute("title", exactTime);
  322. }
  323. });
  324. }
  325.  
  326. function initFixes() {
  327. document.querySelectorAll("form[name=post] th").forEach(e => {
  328. if (e.innerText == "Comment") {
  329. 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>`);
  330. }
  331. });
  332. if (typeof GM_xmlhttpRequest == "function") {
  333. let fileSelectionInterval = setInterval(() => {
  334. if (select = document.querySelector("#upload_selection")) {
  335. select.childNodes[0].insertAdjacentHTML('afterend', ` / <a href="javascript:void(0)" id="sf-file-url"></a>`);
  336. clearInterval(fileSelectionInterval);
  337. }
  338. }, 100);
  339. }
  340. document.addEventListener("dyn_update", e => {
  341. e.detail.forEach(e => fixPost(e));
  342. fixTime();
  343. addExpandos();
  344. });
  345.  
  346. document.addEventListener("post_update", e => {
  347. e.detail.forEach(node => {
  348. if (node.addedNodes[0].nodeName == "DIV") {
  349. fixPost(node.addedNodes[0]);
  350. }
  351. });
  352. fixTime();
  353. addExpandos();
  354. });
  355. [...document.getElementsByClassName("post")].forEach(e => {
  356. fixPost(e);
  357. });
  358. fixTime();
  359. addExpandos();
  360. }
  361.  
  362. // undo filter
  363. function undoFilter(post) {
  364. // if (isEnabled("restore-filtered")) {
  365. // post.querySelectorAll(".body, .body *, .replies, .replies *").forEach(e => {
  366. // e.childNodes.forEach(e => {
  367. // if (e.nodeName == "#text") {
  368. // e.nodeValue = e.nodeValue.replaceAll("im trans btw", "kuz");
  369. // }
  370. // });
  371. // });
  372. // }
  373. }
  374.  
  375. // catalog fixes
  376. document.querySelectorAll("#Grid > div").forEach(e => {
  377. let threadTime = new Date(parseInt(e.getAttribute("data-time")) * 1000);
  378. e.getElementsByClassName("thread-image")[0].setAttribute("title", `${months[threadTime.getMonth()]} ${("0" + threadTime.getDate()).slice(-2)}` +
  379. ` ${("0" + threadTime.getHours()).slice(-2)}:${("0" + threadTime.getMinutes()).slice(-2)}`);
  380. e.querySelectorAll("a").forEach(a => {
  381. a.href = decodeURIComponent(a.href.replace("https://jump.kolyma.net/?", ""));
  382. });
  383. undoFilter(e);
  384. });
  385.  
  386. // overboard reporting
  387. if (document.body.classList.contains("active-ukko")) {
  388. const overboardObserver = new MutationObserver(list => {
  389. const evt = new CustomEvent("overboard_load", {
  390. detail: list
  391. });
  392. document.dispatchEvent(evt);
  393. });
  394. overboardObserver.observe(document.querySelector("form[name=postcontrols]"), {
  395. childList: true,
  396. subtree: true
  397. });
  398.  
  399. let fixOverboardReport = node => {
  400. node.setAttribute("action", "/post.php");
  401. node.lastChild.value = node.closest(".thread").getAttribute("data-board");
  402. };
  403.  
  404. document.addEventListener("overboard_load", e => {
  405. e.detail.forEach(mut => {
  406. if (mut.addedNodes.length == 1) {
  407. let node = mut.addedNodes[0];
  408. if (node.classList && node.classList.contains("post-actions")) {
  409. fixOverboardReport(node);
  410. }
  411. }
  412. });
  413. });
  414. [...document.getElementsByClassName("post-actions")].forEach(e => {
  415. fixOverboardReport(e);
  416. });
  417. }
  418.  
  419. // ctrl + enter to post
  420. window.addEventListener("keydown", e => {
  421. if (e.key == "Enter" && (e.ctrlKey || e.metaKey)) {
  422. if (form = e.target.closest("form[name=post]")) {
  423. form.querySelector("input[type=submit]").click();
  424. }
  425. }
  426. });
  427.  
  428. // autofocus textarea
  429. if ((textarea = document.querySelector("textarea[name=body]")) && document.documentElement.classList.contains("desktop-style") && window.location.hash[1] != "q") {
  430. textarea.focus({
  431. preventScroll: true
  432. });
  433. }
  434.  
  435. // automatically load text captcha
  436. if (script = document.querySelector("td > script")) {
  437. console.log("Loading captcha...");
  438. let variables = Array.from(script.textContent.matchAll(/(?<=")[^",]*(?=")/g), m => m[0]);
  439. actually_load_captcha(variables[0], variables[1]);
  440. }
  441.  
  442. // text captcha solver
  443. function solveCaptcha() {
  444. let bodyColour = window.getComputedStyle(document.body).getPropertyValue("color");
  445. let captcha = document.querySelector(".captcha_html > div");
  446. let captchaBox = captcha.getBoundingClientRect();
  447. let rotationDict = {
  448. "ɐ": "a",
  449. "ə": "e",
  450. "b": "q",
  451. "d": "p",
  452. "n": "u",
  453. "p": "d",
  454. "q": "b",
  455. "u": "n",
  456. 6: 9,
  457. 9: 6,
  458. };
  459. let chars = [];
  460. document.querySelectorAll(".captcha_html div").forEach(e => {
  461. let charBox = e.getBoundingClientRect();
  462. let charStyle = window.getComputedStyle(e);
  463. if (e.innerText.length == 1 &&
  464. charBox.left > captchaBox.left - 5 &&
  465. charBox.right < captchaBox.right + 5 &&
  466. charBox.top > captchaBox.top - 5 &&
  467. charBox.bottom < captchaBox.bottom + 5 &&
  468. parseInt(charStyle.getPropertyValue("font-size")) > 15 &&
  469. parseInt(charStyle.getPropertyValue("height")) > 5 &&
  470. charStyle.getPropertyValue("overflow") != "hidden" &&
  471. charStyle.getPropertyValue("color") == bodyColour) {
  472. let character = e.innerText;
  473. if (charStyle.getPropertyValue("transform").match(/\(-/)) {
  474. if (rotationDict[e.innerText]) {
  475. character = rotationDict[e.innerText];
  476. }
  477. }
  478. chars.push([character, charBox.left]);
  479. }
  480. });
  481. if (chars.length == 3) {
  482. chars.sort((a, b) => (a[1] > b[1]) ? 1 : -1);
  483. document.querySelectorAll(".captcha_text").forEach(e => {
  484. e.value = `${chars[0][0]}${chars[1][0]}${chars[2][0]}`;
  485. });
  486. } else {
  487. console.log(`Failed to solve captcha, got ${chars.length} characters instead of 3.`);
  488. captcha.parentElement.click();
  489. }
  490. }
  491.  
  492. if (passwordBox = document.querySelector("form[name=post] input[name=password]")) {
  493. passwordBox.setAttribute("type", "password");
  494. passwordBox.insertAdjacentHTML("afterend", `<input type="button" name="toggle-password" value="Show">`);
  495. }
  496.  
  497. document.querySelectorAll("form[name=post] input[type=submit]").forEach(e => {
  498. e.setAttribute("og-value", e.getAttribute("value"));
  499. });
  500. setInterval(() => {
  501. let lastTime = getNumber("lastTime");
  502. let difference = 11 - Math.ceil((Date.now() - lastTime) / 1000);
  503. let buttons = document.querySelectorAll("form[name=post] input[type=submit]");
  504. if ([...buttons].find(e => e.value.includes("Post"))) {
  505. return;
  506. } else if (difference > 0) {
  507. let disableButton = isEnabled("disable-submit-on-cooldown");
  508. buttons.forEach(e => {
  509. e.value = `${e.getAttribute("og-value")} (${difference})`;
  510. if (disableButton) {
  511. e.setAttribute("disabled", "disabled");
  512. }
  513. });
  514. } else {
  515. buttons.forEach(e => {
  516. e.value = e.getAttribute("og-value");
  517. e.removeAttribute("disabled");
  518. });
  519. }
  520. }, 100);
  521.  
  522. function areHiddenThreads() {
  523. console.log("poopy");
  524. let threadGrid = document.getElementById("Grid");
  525. if (document.querySelector(".catty-thread.hidden")) {
  526. if (!document.getElementById("toggle-hidden")) {
  527. document.querySelector(".desktop-style #image_size, .mobile-style header").insertAdjacentHTML("afterend", `<span id="toggle-hidden"></span>`);
  528. }
  529. } else if (toggleButton = document.getElementById("toggle-hidden")) {
  530. toggleButton.remove();
  531. document.body.classList.remove("showing-hidden");
  532. }
  533. }
  534.  
  535. if (document.body.classList.contains("active-catalog")) {
  536. if (isEnabled("catalog-navigation")) {
  537. document.querySelectorAll(".boardlist a[href*='index.html']").forEach(e => e.href = e.href.replace("index.html", "catalog.html"));
  538. }
  539. document.querySelector("#image_size").insertAdjacentHTML("afterend", `
  540. <form style="display: inline-block; margin-bottom: 0px;" action="/search.php">
  541. <p>
  542. <input type="text" name="search" placeholder="${board} search">
  543. <input type="hidden" name="board" value="${board}">
  544. <input type="submit" value="Search">
  545. </p>
  546. </form>
  547. `);
  548. let hiddenThreads = getJson("hiddenthreads");
  549. let hasThreads = hiddenThreads.hasOwnProperty(board);
  550. document.querySelectorAll(".mix").forEach(e => {
  551. e.classList.replace("mix", "catty-thread");
  552. if (hasThreads && hiddenThreads[board].hasOwnProperty(e.getAttribute("data-id"))) {
  553. e.classList.add("hidden");
  554. delete hiddenThreads[board][e.getAttribute("data-id")];
  555. }
  556. if (e.getAttribute("data-sticky") == "true") {
  557. e.parentNode.prepend(e);
  558. }
  559. });
  560.  
  561. if (hasThreads) {
  562. Object.keys(hiddenThreads[board]).forEach(e => {
  563. removeFromJson("hiddenthreads", `${board}.${e}`);
  564. });
  565. }
  566. areHiddenThreads();
  567. }
  568.  
  569. document.addEventListener("click", e => {
  570. let t = e.target;
  571. if (t.matches("form[name=post] input[type=submit]")) {
  572. t.value = t.getAttribute("og-value");
  573. if (isEnabled("bypass-filter")) {
  574. let textbox = t.closest("tbody").querySelector("textarea[name=body]");
  575. textbox.value = textbox.value.replaceAll(/(discord)/ig, str => {
  576. let arr = [];
  577. while (!arr.includes("​")) {
  578. arr = [];
  579. [...str].forEach((c, i) => {
  580. if (Math.random() < 0.5 && i != 0) {
  581. arr.push("​");
  582. }
  583. arr.push(c);
  584. if (Math.random() > 0.5 && i != str.length - 1) {
  585. arr.push("​");
  586. }
  587. });
  588. }
  589. return arr.join("");
  590. });
  591. }
  592. } else if (t.matches("input[name=toggle-password]")) {
  593. if (passwordBox.getAttribute("type") == "password") {
  594. passwordBox.setAttribute("type", "text");
  595. t.value = "Hide";
  596. } else {
  597. passwordBox.setAttribute("type", "password");
  598. t.value = "Show";
  599. }
  600. } else if (t.id == "mass-reply") {
  601. let massReply = "";
  602. document.querySelectorAll("[href*='#q']").forEach(e => {
  603. massReply += `>>${e.textContent}\n`;
  604. });
  605. document.querySelectorAll("textarea[name=body]").forEach(e => {
  606. e.value += massReply;
  607. e.focus();
  608. });
  609. } else if (t.id == "mass-quote" || t.id == "mass-orange") {
  610. document.body.classList.add("hide-quote-buttons");
  611. let selection = window.getSelection();
  612. let range = document.createRange();
  613. range.selectNodeContents(document.body);
  614. selection.removeAllRanges();
  615. selection.addRange(range);
  616. let massQuote = window.getSelection().toString().replace(/(\r\n|\r|\n|^)/g, t.id == "mass-quote" ? "$1>" : "$1<") + "\n";
  617. selection.removeAllRanges();
  618. document.body.classList.remove("hide-quote-buttons");
  619. document.querySelectorAll("textarea[name=body]").forEach(e => {
  620. e.value += massQuote;
  621. e.focus();
  622. });
  623. } else if (t.classList.contains("quick-quote") || t.classList.contains("quick-orange")) {
  624. let quote = t.closest(".post").querySelector(".body").innerText.replace(/(\r\n|\r|\n|^)/g, t.classList.contains("quick-quote") ? "$1>" : "$1<") + "\n";
  625. document.querySelectorAll("textarea[name=body]").forEach(e => {
  626. e.value += quote;
  627. e.focus();
  628. });
  629. } else if (t.classList.contains("comment-quote") || t.classList.contains("comment-orange")) {
  630. document.querySelectorAll("textarea[name=body]").forEach(e => {
  631. e.value = e.value.replace(/(\r\n|\r|\n|^)/g, t.classList.contains("comment-quote") ? "$1>" : "$1<");
  632. e.focus();
  633. });
  634. } else if ((e.shiftKey || (e.detail == 3 && (document.documentElement.matches(".mobile-style") || isEnabled("desktop-triple-click")))) &&
  635. t.matches(".active-catalog .catty-thread *, .active-catalog .catty-thread")) {
  636. e.preventDefault();
  637. let thread = t.closest(".catty-thread");
  638. thread.classList.toggle("hidden");
  639. if (thread.classList.contains("hidden")) {
  640. addToJson("hiddenthreads", `${board}.${thread.getAttribute("data-id")}`, Math.floor(Date.now() / 1000));
  641. } else {
  642. removeFromJson("hiddenthreads", `${board}.${thread.getAttribute("data-id")}`);
  643. }
  644. areHiddenThreads();
  645. } else if (t.id == "toggle-hidden") {
  646. document.body.classList.toggle("showing-hidden");
  647. } else if (t.classList.contains("hide-thread-link") || t.classList.contains("unhide-thread-link")) {
  648. setValue("hiddenthreads", localStorage.getItem("hiddenthreads"));
  649. } else if (t.classList.contains("hide-blotter")) {
  650. setValue("hidden-blotter", document.querySelector(".blotter").innerText);
  651. document.body.classList.add("hidden-blotter");
  652. } else if (t.classList.contains("sf-expander")) {
  653. t.closest(".post").setAttribute("manual-cutoff", t.closest(".post").classList.toggle("sf-cutoff"));
  654. } else if (t.classList.contains("sf-formatting-help")) {
  655. let help = `
  656. <span class="quote">&gt;greentext</span><br>
  657. <span class="quote2">&lt;orangetext</span><br>
  658. <span class="heading">==redtext==</span><br>
  659. <span class="heading2">--bluetext--</span><br>
  660. <span class="spoiler">**spoiler**</span><br>
  661. <span class="datamining">%%glowing%%</span><br>
  662. <em>''italics''</em><br>
  663. <b>'''bold'''</b><br>
  664. `;
  665. customAlert(help)
  666. } else if (t.id == "sf-file-url" && !t.classList.contains("sf-loading")) {
  667. let url = window.prompt("Enter a URL: ");
  668. if (!url) return;
  669. t.classList.add("sf-loading");
  670. GM_xmlhttpRequest({
  671. method: "GET",
  672. url: url,
  673. responseType: "blob",
  674. onload: response => {
  675. if (response.status != 200) {
  676. customAlert("Can't read file");
  677. t.classList.remove("sf-loading");
  678. return;
  679. }
  680. if (!(response.response instanceof Blob)) {
  681. customAlert("Something went wrong. Using AdGuard as your userscript manager? This feature is currently broken due to its GM_xmlhttpRequest implementation returning the wrong data type.");
  682. return;
  683. }
  684. let responseHeaders = {};
  685. response.responseHeaders.trim().split("\r\n").forEach(e => {
  686. let split = e.split(":", 2);
  687. responseHeaders[split[0].toLowerCase()] = split[1].trim();
  688. });
  689. let contentType = responseHeaders["content-type"];
  690. let validMime = contentType != "application/octet-stream";
  691. let headerFilename = responseHeaders["content-disposition"]?.match(/(?<=filename=").*(?=")/);
  692. let filename = headerFilename ? headerFilename[0] : url.split("/").pop().split("?")[0];
  693. let fileExt = filename?.match(/(?<=\.)[a-zA-Z0-9]{2,4}$/);
  694. if (!validMime && fileExt) {
  695. let mime = fileToMime[fileExt[0]];
  696. if (mime) {
  697. contentType = mime;
  698. } else {
  699. customAlert("Warning: could not guess MIME type.")
  700. }
  701. }
  702. if (!fileExt) {
  703. if (validMime) {
  704. filename += `.${contentType.split("/")[1]}`;
  705. } else {
  706. customAlert("Failure: could not guess file extension.");
  707. return;
  708. }
  709. }
  710. let file = new File([response.response], filename, {lastModified: Date.now(), type: contentType})
  711. let dataTransfer = new DataTransfer();
  712. dataTransfer.items.add(file);
  713. document.dispatchEvent(new DragEvent("drop", {"dataTransfer": dataTransfer}));
  714. t.classList.remove("sf-loading");
  715. }
  716. });
  717. }
  718. });
  719.  
  720. document.addEventListener("input", e => {
  721. let t = e.target;
  722. if (t.matches("input[name=search]") && document.querySelector(".sf-catty, .active-catalog")) {
  723. document.querySelectorAll(".catty-thread").forEach(e => {
  724. if (e.innerText.toLowerCase().includes(t.value.toLowerCase())) {
  725. e.classList.remove("sf-filtered");
  726. } else {
  727. e.classList.add("sf-filtered");
  728. }
  729. });
  730. }
  731. });
  732.  
  733. window.addEventListener("load", () => {
  734. if (isEnabled("autofill-captcha") && document.querySelector(".captcha_html")) {
  735. const captchaObserver = new MutationObserver(list => {
  736. const evt = new CustomEvent("captcha_load", {
  737. detail: list
  738. });
  739. document.dispatchEvent(evt);
  740. });
  741. captchaObserver.observe(document.querySelector(".captcha_html"), {
  742. childList: true
  743. });
  744.  
  745. if (document.querySelector(".captcha_html > div")) {
  746. console.log("Captcha already exists, solving.");
  747. solveCaptcha();
  748. }
  749. document.addEventListener("captcha_load", () => {
  750. solveCaptcha();
  751. });
  752. }
  753. });
  754.  
  755. if (getValue("hover-images") && document.documentElement.matches(".desktop-style")) {
  756. let adjustHoverPos = (hover, x, y) => {
  757. if (!document.querySelector(".post-image:hover, .thread-image:hover")) {
  758. hover.remove();
  759. }
  760. if (x > document.documentElement.clientWidth / 2) {
  761. hover.style.maxWidth = `${x - 5}px`;
  762. hover.style.left = "";
  763. hover.style.right = `${document.documentElement.clientWidth - x + 5}px`
  764. } else {
  765. hover.style.left = `${x + 5}px`
  766. hover.style.right = "";
  767. hover.style.maxWidth = `${document.documentElement.clientWidth - x + 5}px`;
  768. }
  769. hover.style.maxHeight = `${document.documentElement.clientHeight}px`;
  770. if (hover.naturalHeight > document.documentElement.clientHeight) {
  771. hover.style.top = "0px";
  772. } else {
  773. if (y < hover.naturalHeight / 2) {
  774. hover.style.top = `0px`;
  775. } else if (document.documentElement.clientHeight - y < hover.naturalHeight / 2) {
  776. hover.style.top = `${document.documentElement.clientHeight - (hover.naturalHeight)}px`;
  777. } else {
  778. hover.style.top = `${y - (hover.naturalHeight / 2)}px`;
  779. }
  780. }
  781. }
  782.  
  783. document.addEventListener("mouseover", e => {
  784. let t = e.target;
  785. if (((!document.body.classList.contains("active-catalog") && !document.querySelector("#dyn-content.theme-catalog")) || isEnabled("hover-on-catalog")) &&
  786. t.matches(".post-image, .thread-image") && !t.closest(".image-hidden") && !document.querySelector("#sharty-hover")) {
  787. let srcImg = t.getAttribute("src").replace("thumb", "src");
  788. if (srcImg.match("spoiler") && !document.body.classList.contains("active-catalog")) {
  789. srcImg = t.closest("#dyn-content.theme-catalog") ? t.parentNode.getAttribute("file-source") : t.parentNode.href;
  790. } else if (srcImg.match("deleted") || srcImg.match("spoiler")) {
  791. return;
  792. }
  793. document.body.insertAdjacentHTML("beforeend", `<img id="sharty-hover" onerror="this.style.display='none'" src=${srcImg}>`);
  794. let hover = document.getElementById("sharty-hover");
  795. let poll = setInterval(() => {
  796. if (hover.naturalWidth && hover.naturalHeight) {
  797. clearInterval(poll);
  798. adjustHoverPos(hover, e.x, e.y);
  799. }
  800. }, 10);
  801. }
  802. });
  803.  
  804. document.addEventListener("mousemove", e => {
  805. if (hover = document.querySelector("#sharty-hover")) {
  806. adjustHoverPos(hover, e.x, e.y);
  807. }
  808. });
  809.  
  810. document.addEventListener("mouseout", e => {
  811. let t = e.target;
  812. if ((hover = document.querySelector("#sharty-hover")) && t.matches(".post-image, .thread-image")) {
  813. hover.remove();
  814. }
  815. });
  816. }
  817.  
  818. if (blotter = document.querySelector(".blotter")) {
  819. blotter.insertAdjacentHTML("beforebegin", `<a class="hide-blotter" href="javascript:void(0)">[–]</a>`);
  820. if (blotter.innerText == getValue("hidden-blotter") || isEnabled("hide-blotter")) {
  821. document.body.classList.add("hidden-blotter");
  822. }
  823. }
  824.  
  825.  
  826. document.head.insertAdjacentHTML("beforeend", `
  827. <style>
  828. .hide-blotter {
  829. float: left;
  830. }
  831.  
  832. .hidden-blotter .blotter,
  833. .hidden-blotter .blotter + hr,
  834. .hidden-blotter .hide-blotter,
  835. .catty-thread.hidden,
  836. .showing-hidden .catty-thread,
  837. .mobile-style .g-recaptcha-bubble-arrow,
  838. .catty-thread.sf-filtered,
  839. .showing-hidden .catty-thread.hidden.sf-filtered,
  840. .hide-quote-buttons .quote-buttons {
  841. display: none !important;
  842. }
  843.  
  844. .reply .sf-expander {
  845. margin-left: 1.8em;
  846. padding-right: 3em;
  847. padding-bottom: 0.3em;
  848. }
  849.  
  850. .sf-expander::after {
  851. content: "[Hide Full Text]";
  852. }
  853.  
  854. .sf-cutoff .sf-expander::after {
  855. content: "[Show Full Text]";
  856. }
  857.  
  858. #sf-file-url::after {
  859. content: "URL";
  860. }
  861.  
  862. #sf-file-url.sf-loading::after {
  863. content: "Loading...";
  864. }
  865.  
  866. #sharty-hover {
  867. pointer-events: none;
  868. position: fixed;
  869. z-index: 500;
  870. }
  871.  
  872. .catty-thread, .showing-hidden .catty-thread.hidden {
  873. display: inline-block !important;
  874. }
  875.  
  876. #toggle-hidden {
  877. text-decoration: underline;
  878. color: #34345C;
  879. cursor: pointer;
  880. user-select: none;
  881. }
  882.  
  883. #toggle-hidden::before {
  884. content: "[Show Hidden]";
  885. }
  886.  
  887. .showing-hidden #toggle-hidden::before {
  888. content: "[Hide Hidden]";
  889. }
  890.  
  891. #image_size + #toggle-hidden {
  892. display: inline-block;
  893. padding-left: 5px;
  894. }
  895.  
  896. header + #toggle-hidden {
  897. display: block;
  898. margin: 1em auto;
  899. width: fit-content;
  900. }
  901.  
  902. .mobile-style .g-recaptcha-bubble-arrow + div {
  903. position: fixed !important;
  904. left: 50%;
  905. top: 50%;
  906. transform: translate(-50%, -50%);
  907. -webkit-transform: translate(-50%, -50%);
  908. }
  909.  
  910. input[name=toggle-password] {
  911. margin-left: 2px;
  912. }
  913.  
  914. .sf-formatting-help {
  915. text-decoration: underline;
  916. cursor: pointer;
  917. }
  918.  
  919. .comment-quotes {
  920. text-align: center;
  921. }
  922.  
  923. ${isEnabled("truncate-long-posts") ? `
  924. .sf-cutoff:not(.post-hover) .body {
  925. overflow: hidden;
  926. word-break: break-all;
  927. display: -webkit-box;
  928. min-width: min-content;
  929. -webkit-line-clamp: 20;
  930. -webkit-box-orient: vertical;
  931. }
  932.  
  933. .sf-cutoff.post-hover .sf-expander {
  934. display: none !important;
  935. }
  936.  
  937. div.post.reply.sf-cutoff div.body {
  938. margin-bottom: 0.3em;
  939. padding-bottom: unset;
  940. }
  941. ` : ""};
  942. </style>
  943. `);
  944.  
  945. initFixes();