Guest User

8chanSS Vertical List

a guest
Jun 5th, 2025
11
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 259.13 KB | None | 0 0
  1. // ==UserScript==
  2. // @name 8chanSS
  3. // @version 1.52.1
  4. // @namespace 8chanss
  5. // @description A userscript to add functionality to 8chan.
  6. // @author otakudude
  7. // @minGMVer 4.3
  8. // @minFFVer 121
  9. // @license MIT; https://github.com/otacoo/8chanSS/blob/main/LICENSE
  10. // @match *://8chan.moe/*
  11. // @match *://8chan.se/*
  12. // @exclude *://8chan.moe/login.html
  13. // @exclude *://8chan.se/login.html
  14. // @grant GM.getValue
  15. // @grant GM.setValue
  16. // @grant GM.deleteValue
  17. // @grant GM.listValues
  18. // @grant GM.xmlHttpRequest
  19. // @connect youtube.com
  20. // @connect i.ytimg.com
  21. // @run-at document-start
  22. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAZlBMVEUAAABdlloAhl758AH58AH58AcAhl758ADj4CYAh14AhV4AhV0Ahl748AcChl4Chl0Ab2H58AIAhl758AD58AAAhl757wL48AD47wL78QL47wcAh1748AF3oFfs5yEAh1/68QDz7BM5qSu8AAAAH3RSTlMA/lg/OYtM8g/onXtoXzAaCdzBsIFzczMeaCXXyrmp9ddA3QAAANpJREFUSMft0tkOgjAQheFjtVCQVVxwnfr+L+kWM5FOC73TxP/6fBedFJwpyx5CtSpqSHXWpns4qYxo1cDtkNp7GoOW9KgSwM4+09KeEhmw4H0IuGJDAbCw79a8nwJYFDQCuO1gT8oLWCiKAXavKA5cZ78I5n/wBx7wfb+1TwOggpD2gxxSpvWBrIbY3AcUPK1lkMNbJ4FV4wd964KsQqBF6oAEwcoh2GAk/QlyjNYx4AeHMicGxxoTOrRvIB5IPtULJJhY+QIFJrd9gCUi0tdZjqgu5yYOGAO5G/kyc3TkciPeAAAAAElFTkSuQmCC
  23. // @downloadURL https://update.greasyfork.org/scripts/533268/8chanSS.user.js
  24. // @updateURL https://update.greasyfork.org/scripts/533268/8chanSS.meta.js
  25. // ==/UserScript==
  26. function onReady(fn) {
  27. if (document.readyState === "loading") {
  28. document.addEventListener("DOMContentLoaded", fn, { once: true });
  29. } else {
  30. fn();
  31. }
  32. }
  33. const debounce = (fn, delay) => {
  34. let timeout;
  35. return (...args) => {
  36. clearTimeout(timeout);
  37. timeout = setTimeout(() => fn.apply(this, args), delay);
  38. };
  39. };
  40. const observerRegistry = {};
  41.  
  42. function observeSelector(selector, options = { childList: true, subtree: false }) {
  43. if (observerRegistry[selector]) return observerRegistry[selector];
  44.  
  45. const node = document.querySelector(selector);
  46. if (!node) return null;
  47.  
  48. const handlers = [];
  49. const observer = new MutationObserver(mutations => {
  50. for (const handler of handlers) {
  51. try {
  52. handler(mutations, node);
  53. } catch (e) {
  54. console.error(`Observer handler error for ${selector}:`, e);
  55. }
  56. }
  57. });
  58.  
  59. observer.observe(node, options);
  60. window.addEventListener('beforeunload', () => observer.disconnect());
  61.  
  62. observerRegistry[selector] = {
  63. node,
  64. observer,
  65. handlers,
  66. addHandler: fn => handlers.push(fn)
  67. };
  68. return observerRegistry[selector];
  69. }
  70. window.pageType = (() => {
  71. const path = window.location.pathname.toLowerCase();
  72. const currentHost = window.location.hostname.toLowerCase();
  73.  
  74. return {
  75. isCatalog: /\/catalog\.html$/i.test(path),
  76. isThread: /\/(res|last)\/[^/]+\.html$/i.test(path),
  77. isLast: /\/last\/[^/]+\.html$/i.test(path),
  78. isIndex: /\/[^/]+\/$/i.test(path),
  79. is8chan: /^8chan\.(se|moe)$/.test(currentHost),
  80. host: currentHost,
  81. path: path
  82. };
  83. })();
  84. (function injectCssAsap() {
  85. function doInject() {
  86. if (document.getElementById('8chSShim')) return;
  87. if (!document.head) {
  88. setTimeout(doInject, 1);
  89. return;
  90. }
  91. const style = document.createElement('style');
  92. style.id = '8chSShim';
  93. style.textContent = "#dynamicAnnouncement,#panelMessage,#postingForm{visibility:hidden}:not(.is-catalog) body{margin:0}.innerUtility.top{margin-top:2em;background:0 0!important;color:var(--link-color)!important}.innerUtility.top a{color:var(--link-color)!important}";
  94. document.head.appendChild(style);
  95. }
  96. doInject();
  97. })();
  98. onReady(async function () {
  99. "use strict";
  100. const divThreads = document.getElementById('divThreads');
  101. const innerOP = document.querySelector('.innerOP');
  102. const divPosts = document.querySelector('.divPosts');
  103. const opHeadTitle = document.querySelector('.opHead.title');
  104. const catalogDiv = document.querySelector('.catalogDiv');
  105. const VERSION = "1.52.1";
  106. const scriptSettings = {
  107. site: {
  108. _siteTWTitle: { type: "title", label: ":: Thread Watcher" },
  109. _siteSection1: { type: "separator" },
  110. alwaysShowTW: {
  111. label: "Pin Thread Watcher",
  112. default: false,
  113. subOptions: {
  114. noPinInCatalog: {
  115. label: "Don't pin in Catalog",
  116. default: false,
  117. }
  118. }
  119. },
  120. autoExpandTW: { label: "Auto Expand Thread Watcher", default: false },
  121. _siteSiteTitle: { type: "title", label: ":: Site" },
  122. _siteSection2: { type: "separator" },
  123. customFavicon: {
  124. label: "Custom Favicon",
  125. default: false,
  126. subOptions: {
  127. faviconStyle: {
  128. label: "Favicon Style",
  129. type: "select",
  130. default: "default",
  131. options: [
  132. { value: "default", label: "Default" },
  133. { value: "pixel", label: "Pixel" },
  134. { value: "pixel_alt", label: "Pixel Alt" },
  135. { value: "eight", label: "Eight" },
  136. { value: "eight_dark", label: "Eight Dark" }
  137. ]
  138. }
  139. }
  140. },
  141. enableBottomHeader: { label: "Bottom Header", default: false },
  142. enableAutoHideHeaderScroll: { label: "Auto-hide Header On Scroll", default: false },
  143. enableHeaderCatalogLinks: {
  144. label: "Header Catalog Links",
  145. default: true,
  146. subOptions: {
  147. openInNewTab: {
  148. label: "Always open in new tab",
  149. default: false,
  150. }
  151. }
  152. },
  153. enableScrollArrows: { label: "Show Up/Down Arrows", default: false },
  154. _siteMediaTitle: { type: "title", label: ":: Media" },
  155. _siteSection3: { type: "separator" },
  156. enableThreadImageHover: { label: "Thread Image Hover", default: true },
  157. blurSpoilers: {
  158. label: "Blur Spoilers",
  159. default: false,
  160. subOptions: {
  161. removeSpoilers: {
  162. label: "Remove Spoilers",
  163. default: false
  164. }
  165. }
  166. },
  167. enableMediaViewer: {
  168. label: "Enable Advanced Media Viewer",
  169. default: false,
  170. subOptions: {
  171. viewerStyle: {
  172. label: "Style",
  173. type: "select",
  174. default: "native",
  175. options: [
  176. { value: "native", label: "Native" },
  177. { value: "topright", label: "Pin Top Right" },
  178. { value: "topleft", label: "Pin Top Left" }
  179. ]
  180. }
  181. }
  182. },
  183. hoverVideoVolume: { label: "Hover Media Volume (0-100%)", default: 50, type: "number", min: 0, max: 100 }
  184. },
  185. threads: {
  186. _threadsNotiTitle: { type: "title", label: ":: Notifications" },
  187. _threadsSection1: { type: "separator" },
  188. beepOnYou: { label: "Beep on (You)", default: false },
  189. notifyOnYou: {
  190. label: "Tab Notification when (You) (!)",
  191. default: true,
  192. subOptions: {
  193. customMessage: {
  194. label: "Custom Text (max: 8 chars.)",
  195. default: "",
  196. type: "text",
  197. maxLength: 9
  198. }
  199. }
  200. },
  201. _threadsNavTitle: { type: "title", label: ":: Navigation & Others" },
  202. _threadsSection3: { type: "separator" },
  203. enableScrollSave: {
  204. label: "Save Scroll Position",
  205. default: true,
  206. subOptions: {
  207. showUnreadLine: {
  208. label: "Show Unread Line",
  209. default: true,
  210. }
  211. }
  212. },
  213. quoteThreading: { label: "Quote Threading", default: false },
  214. enableHashNav: { label: "Hash Navigation", default: false },
  215. threadStatsInHeader: { label: "Thread Stats in Header", default: false },
  216. watchThreadOnReply: { label: "Watch Thread on Reply", default: true },
  217. scrollToBottom: { label: "Don't Scroll to Bottom on Reply", default: true }
  218. },
  219. catalog: {
  220. enableCatalogImageHover: { label: "Catalog Image Hover", default: true },
  221. enableThreadHiding: { label: "Enable Thread Hiding", default: false },
  222. openCatalogThreadNewTab: { label: "Always Open Threads in New Tab", default: false },
  223. enableLastFifty: { label: "Show Last 50 Posts button", default: false }
  224. },
  225. styling: {
  226. _stylingSiteTitle: { type: "title", label: ":: Site Styling" },
  227. _stylingSection1: { type: "separator" },
  228. hideAnnouncement: { label: "Hide Announcement", default: false },
  229. hidePanelMessage: { label: "Hide Panel Message", default: false },
  230. hidePostingForm: {
  231. label: "Hide Posting Form",
  232. default: false,
  233. subOptions: {
  234. showCatalogForm: {
  235. label: "Don't Hide in Catalog",
  236. default: false
  237. }
  238. }
  239. },
  240. hideBanner: { label: "Hide Board Banners", default: false },
  241. hideDefaultBL: { label: "Hide Default Board List", default: true },
  242. hideNoCookieLink: { label: "Hide No Cookie? Link", default: false },
  243. hideJannyTools: { label: "Hide Janitor Forms", default: false },
  244. _stylingThreadTitle: { type: "title", label: ":: Thread Styling" },
  245. _stylingSection2: { type: "separator" },
  246. highlightOnYou: { label: "Style (You) posts", default: true },
  247. opBackground: { label: "OP background", default: false },
  248. enableStickyQR: { label: "Sticky Quick Reply", default: false },
  249. fadeQuickReply: { label: "Fade Quick Reply", default: false },
  250. enableFitReplies: { label: "Fit Replies", default: false },
  251. enableSidebar: {
  252. label: "Enable Sidebar",
  253. default: false,
  254. subOptions: {
  255. leftSidebar: {
  256. label: "Sidebar on Left",
  257. default: false
  258. }
  259. }
  260. },
  261. threadHideCloseBtn: { label: "Hide Inline Close Button", default: false },
  262. hideCheckboxes: { label: "Hide Post Checkbox", default: false }
  263. },
  264. miscel: {
  265. enableShortcuts: { label: "Enable Keyboard Shortcuts", type: "checkbox", default: true },
  266. enableUpdateNotif: { label: "8chanSS update notifications", default: true },
  267. enhanceYoutube: {
  268. label: "Enhanced Youtube Links",
  269. default: true,
  270. subOptions: {
  271. ytThumbs: {
  272. label: "Show Thumbnails on Hover",
  273. default: true,
  274. }
  275. }
  276. },
  277. switchTimeFormat: { label: "Enable 12-hour Clock (AM/PM)", default: false },
  278. truncFilenames: {
  279. label: "Truncate filenames",
  280. default: false,
  281. subOptions: {
  282. customTrunc: {
  283. label: "Max filename length (min: 5, max: 50)",
  284. default: 15,
  285. type: "number",
  286. min: 5,
  287. max: 50
  288. }
  289. }
  290. },
  291. enableTheSauce: {
  292. label: "Sauce Links",
  293. default: false,
  294. subOptions: {
  295. iqdb: {
  296. label: "IQDB",
  297. default: false,
  298. },
  299. saucenao: {
  300. label: "Saucenao",
  301. default: false,
  302. },
  303. pixiv: {
  304. label: "Pixiv (only added if filename matches Pixiv ID)",
  305. default: false,
  306. }
  307. }
  308. },
  309. _miscelFilterTitle: { type: "title", label: ":: Filtering" },
  310. _miscelSection1: { type: "separator" },
  311. enableHidingMenu: {
  312. label: "Alternative post hiding menu & features",
  313. default: false,
  314. subOptions: {
  315. recursiveHide: {
  316. label: "Recursive hide/filter/name+ (hide replies to replies)",
  317. default: false
  318. }
  319. }
  320. },
  321. hideHiddenPostStub: { label: "Hide Stubs of Hidden Posts", default: false, },
  322. _miscelIDTitle: { type: "title", label: ":: IDs" },
  323. _miscelSection2: { type: "separator" },
  324. highlightNewIds: {
  325. label: "Highlight New IDs",
  326. default: false,
  327. subOptions: {
  328. idHlStyle: {
  329. label: "Highlight Style",
  330. type: "select",
  331. default: "moetext",
  332. options: [
  333. { value: "moetext", label: "Moe" },
  334. { value: "glow", label: "Glow" },
  335. { value: "dotted", label: "Border" }
  336. ]
  337. }
  338. }
  339. },
  340. alwaysShowIdCount: { label: "Always show post count for IDs", default: false },
  341. enableIdFilters: {
  342. label: "Show only posts by ID when ID is clicked",
  343. type: "checkbox",
  344. default: true,
  345. subOptions: {
  346. showIdLinksOnly: {
  347. label: "Show as a floating list",
  348. default: false
  349. },
  350. showIdLinksVertical: {
  351. label: "Show posts in a vertical list",
  352. default: false
  353. }
  354. }
  355. },
  356. enableIdToggle: { label: "Add menu entry to toggle IDs as Yours", type: "checkbox", default: false }
  357. }
  358. };
  359.  
  360. Object.freeze(scriptSettings);
  361. function flattenSettings() {
  362. const result = {};
  363. Object.keys(scriptSettings).forEach((category) => {
  364. Object.keys(scriptSettings[category]).forEach((key) => {
  365. if (key.startsWith('_')) return;
  366. result[key] = scriptSettings[category][key];
  367. const subOptions = scriptSettings[category][key].subOptions;
  368. if (!subOptions || typeof subOptions !== "object") return;
  369. Object.keys(subOptions).forEach((subKey) => {
  370. const fullKey = `${key}_${subKey}`;
  371. result[fullKey] = subOptions[subKey];
  372. });
  373. });
  374. });
  375. return Object.freeze(result);
  376. }
  377. let flatSettings = flattenSettings();
  378. async function getSetting(key) {
  379. if (!flatSettings[key]) {
  380. console.warn(`Setting key not found: ${key}`);
  381. return false;
  382. }
  383. let val;
  384. try {
  385. val = await GM.getValue("8chanSS_" + key, null);
  386. } catch (err) {
  387. console.error(`Failed to get setting for key ${key}:`, err);
  388. return flatSettings[key]?.default ?? false;
  389. }
  390. if (val === null) return flatSettings[key].default;
  391. switch (flatSettings[key].type) {
  392. case "number":
  393. return Number(val);
  394. case "text":
  395. return String(val).replace(/[<>"']/g, "").slice(0, flatSettings[key].maxLength || 32);
  396. case "textarea":
  397. case "select":
  398. return String(val);
  399. default:
  400. return val === "true";
  401. }
  402. }
  403.  
  404. async function setSetting(key, value) {
  405. try {
  406. await GM.setValue("8chanSS_" + key, String(value));
  407. } catch (err) {
  408. console.error(`Failed to set setting for key ${key}:`, err);
  409. }
  410. }
  411. (async function featureCssClassToggles() {
  412. document.documentElement.classList.add("8chanSS");
  413. const enableSidebar = await getSetting("enableSidebar");
  414. const enableSidebar_leftSidebar = await getSetting("enableSidebar_leftSidebar");
  415.  
  416. const classToggles = {
  417. enableFitReplies: "fit-replies",
  418. enableSidebar_leftSidebar: "ss-leftsidebar",
  419. enableStickyQR: "sticky-qr",
  420. fadeQuickReply: "fade-qr",
  421. enableBottomHeader: "bottom-header",
  422. hideHiddenPostStub: "hide-stub",
  423. hideBanner: "disable-banner",
  424. hidePostingForm: "hide-posting-form",
  425. hidePostingForm_showCatalogForm: "show-catalog-form",
  426. hideDefaultBL: "hide-defaultBL",
  427. hidePanelMessage: "hide-panelmessage",
  428. highlightOnYou: "highlight-yous",
  429. threadHideCloseBtn: "hide-close-btn",
  430. hideCheckboxes: "hide-checkboxes",
  431. hideNoCookieLink: "hide-nocookie",
  432. autoExpandTW: "auto-expand-tw",
  433. hideJannyTools: "hide-jannytools",
  434. opBackground: "op-background",
  435. blurSpoilers: "ss-blur-spoilers"
  436. };
  437. if (enableSidebar && !enableSidebar_leftSidebar) {
  438. document.documentElement.classList.add("ss-sidebar");
  439. } else {
  440. document.documentElement.classList.remove("ss-sidebar");
  441. }
  442. const settingKeys = Object.keys(classToggles);
  443. const settingValues = await Promise.all(settingKeys.map(getSetting));
  444. settingKeys.forEach((key, i) => {
  445. const className = classToggles[key];
  446. if (settingValues[i]) {
  447. document.documentElement.classList.add(className);
  448. } else {
  449. document.documentElement.classList.remove(className);
  450. }
  451. });
  452. if (window.pageType?.isCatalog) {
  453. document.documentElement.classList.add("is-catalog");
  454. } else {
  455. document.documentElement.classList.remove("is-catalog");
  456. }
  457. if (window.pageType?.isThread) {
  458. document.documentElement.classList.add("is-thread");
  459. } else {
  460. document.documentElement.classList.remove("is-thread");
  461. }
  462. if (window.pageType?.isIndex) {
  463. document.documentElement.classList.add("is-index");
  464. } else {
  465. document.documentElement.classList.remove("is-index");
  466. }
  467. })();
  468. (async function featureSidebar() {
  469. const enableSidebar = await getSetting("enableSidebar");
  470. const enableSidebar_leftSidebar = await getSetting("enableSidebar_leftSidebar");
  471.  
  472. const mainPanel = document.getElementById("mainPanel");
  473. if (!mainPanel) return;
  474.  
  475. if (enableSidebar && enableSidebar_leftSidebar) {
  476. mainPanel.style.marginLeft = "19rem";
  477. mainPanel.style.marginRight = "0";
  478. } else if (enableSidebar) {
  479. mainPanel.style.marginRight = "19rem";
  480. mainPanel.style.marginLeft = "0";
  481. } else {
  482. mainPanel.style.marginRight = "0";
  483. mainPanel.style.marginLeft = "0";
  484. }
  485. })();
  486. (function injectCustomCss() {
  487. if (document.getElementById('8chSS')) return;
  488.  
  489. let css = "";
  490. if (window.pageType?.is8chan) {
  491. css += "#dynamicAnnouncement,#panelMessage,#postingForm{visibility:visible}#navFadeEnd,#navFadeMid,.watchedNotification::before,:root.disable-banner #bannerImage,:root.hide-announcement #dynamicAnnouncement,:root.hide-checkboxes .deletionCheckBox,:root.hide-close-btn .inlineQuote>.innerPost>.postInfo.title>a:first-child,:root.hide-jannytools #actionsForm,:root.hide-jannytools #boardContentLinks,:root.hide-nocookie #captchaBody>table:nth-child(2)>tbody:first-child>tr:nth-child(2),:root.hide-panelmessage #panelMessage,:root.hide-posting-form #postingForm{display:none}#sideCatalogDiv{z-index:200;background:var(--background-gradient)}:root.hide-defaultBL #navTopBoardsSpan{display:none!important}:root.is-catalog.show-catalog-form #postingForm{display:block!important}:root.is-thread footer{visibility:hidden;height:0}:root.op-background .opCell>.innerOP{padding-top:.25em;width:100%;background:var(--contrast-color);border:1px solid var(--horizon-sep-color);border-top-width:0;border-left-width:0}nav.navHeader{z-index:300}nav.navHeader>.nav-boards:hover{overflow-x:auto;overflow-y:hidden;scrollbar-width:thin}:not(:root.bottom-header) .navHeader{box-shadow:0 1px 2px rgba(0,0,0,.15)}:root.bottom-header nav.navHeader{top:auto!important;bottom:0!important;box-shadow:0 -1px 2px rgba(0,0,0,.15)}:root.highlight-yous .innerOP:has(> .opHead.title > .youName),:root.highlight-yous .innerPost:has(> .postInfo.title > .youName),:root.highlight-yous .yourPost{border-left:dashed #34a4eb 2px!important}:root.highlight-yous .innerPost:has(>.divMessage>.you),:root.highlight-yous .innerPost:has(>.divMessage>:not(div)>.you),:root.highlight-yous .innerPost:has(>.divMessage>:not(div)>:not(div)>.you),:root.highlight-yous .quotesYou{border-left:solid red 2px!important}:root.fit-replies :not(.hidden).innerPost{margin-left:10px;display:flow-root}:root.fit-replies :not(.hidden,.inlineQuote).innerPost{margin-left:0}.originalNameLink{display:inline;overflow-wrap:anywhere;white-space:normal}.multipleUploads .uploadCell:not(.expandedCell){max-width:215px}:root.ss-blur-spoilers .quoteTooltip img[src*=\"audioGenericThumb\.png\"],:root.ss-blur-spoilers .quoteTooltip img[src*=\"custom\.spoiler\"],:root.ss-blur-spoilers .quoteTooltip img[src*=\"spoiler\.png\"]{filter:blur(5px)!important;transition:filter .3s ease}:not(#media-viewer)>.imgExpanded,:not(#media-viewer)>video{max-height:90vh!important;object-fit:contain;width:auto!important}:not(:root.auto-expand-tw) #watchedMenu .floatingContainer{overflow-x:hidden;overflow-wrap:break-word}:root.auto-expand-tw #watchedMenu .floatingContainer{height:fit-content!important;padding-bottom:10px}.watchedCellLabel a::before{content:attr(data-board);color:#aaa;margin-right:4px;font-weight:700}.watchButton.watched-active::before{color:#dd003e!important}#media-viewer,#multiboardMenu,#settingsMenu,#watchedMenu{font-size:smaller;padding:5px!important;box-shadow:-3px 3px 2px 0 rgba(0,0,0,.19)}#watchedMenu,#watchedMenu .floatingContainer{min-width:200px;max-width:100vw}.watchedNotification::before{padding-right:2px}#watchedMenu .floatingContainer{scrollbar-width:thin;scrollbar-color:var(--link-color) var(--contrast-color)}.scroll-arrow-btn{position:fixed;right:50px;width:36px;height:35px;background:#222;color:#fff;border:none;border-radius:50%;box-shadow:0 2px 8px rgba(0,0,0,.18);font-size:22px;cursor:pointer;opacity:.7;z-index:800;display:flex;align-items:center;justify-content:center;transition:opacity .2s,background .2s}:root:not(.is-index,.is-catalog).ss-sidebar .scroll-arrow-btn{right:330px!important}.scroll-arrow-btn:hover{opacity:1;background:#444}#scroll-arrow-up{bottom:80px}#scroll-arrow-down{bottom:32px}.bumpLockIndicator::after{padding-right:3px}.floatingMenu.focused{z-index:305!important}#quick-reply{padding:0}#media-viewer{padding:20px 0 0!important}#media-viewer.topright{top:26px!important;right:0!important;left:auto!important;max-height:97%!important;max-width:max-content!important}#media-viewer.topleft{top:26px!important;left:0!important;right:auto!important;max-height:97%!important;max-width:max-content!important}#media-viewer.topright::after{pointer-events:none}#media-viewer.topleft::after{pointer-events:none}.ss-chevron{transition:transform .2s;margin-left:6px;font-size:12px;display:inline-block}a.imgLink[data-filemime^='audio/'],a.originalNameLink[href$='.m4a'],a.originalNameLink[href$='.mp3'],a.originalNameLink[href$='.ogg'],a.originalNameLink[href$='.wav']{position:relative}.audio-preview-indicator{display:none;position:absolute;background:rgba(0,0,0,.7);color:#fff;padding:5px;font-size:12px;border-radius:3px;z-index:1000;left:0;top:0;white-space:nowrap;pointer-events:none}a.originalNameLink:hover .audio-preview-indicator,a[data-filemime^='audio/']:hover .audio-preview-indicator{display:block}.yt-icon{width:16px;height:13px;vertical-align:middle;margin-right:2px}.id-glow{box-shadow:0 0 12px var(--subject-color)}.id-dotted{border:2px dotted #fff}";
  492. }
  493. if (window.pageType?.isThread) {
  494. css += ":root.sticky-qr #quick-reply{display:block;top:auto!important;bottom:0}:root.sticky-qr.ss-sidebar #quick-reply{left:auto!important;right:0!important}:root.sticky-qr.ss-leftsidebar #quick-reply{left:0!important;right:auto!important}:root.sticky-qr #qrbody{resize:vertical;max-height:50vh;height:130px}#selectedDivQr,:root.sticky-qr #selectedDiv{display:inline-flex;overflow:scroll hidden;max-width:300px}#qrbody{min-width:300px}:root.bottom-header #quick-reply{bottom:28px!important}:root.fade-qr #quick-reply{padding:0;opacity:.7;transition:opacity .3s ease}:root.fade-qr #quick-reply:focus-within,:root.fade-qr #quick-reply:hover{opacity:1}#qrFilesBody{max-width:310px}#quick-reply{box-shadow:-3px 3px 2px 0 rgba(0,0,0,.19)}#unread-line{height:2px;border:none!important;pointer-events:none!important;background-image:linear-gradient(to left,rgba(185,185,185,.2),var(--text-color),rgba(185,185,185,.2));margin:-3px auto -3px auto;width:60%}:root.ss-sidebar #bannerImage{width:19rem;right:0;position:fixed;top:26px}:root.ss-sidebar.bottom-header #bannerImage{top:0!important}:root.ss-leftsidebar #bannerImage{width:19rem;left:0;position:fixed;top:26px}:root.ss-leftsidebar.bottom-header #bannerImage{top:0!important}.quoteTooltip{z-index:999}.nestedQuoteLink{text-decoration:underline dashed!important}:root.hide-stub .unhideButton{display:none}.quoteTooltip .innerPost{overflow:hidden}.inlineQuote .innerPost,.quoteTooltip .innerPost{box-shadow:-1px 1px 2px 0 rgba(0,0,0,.19)}.inlineQuote{margin-top:4px}.postInfo.title>.inlineQuote{margin-left:15px}.postCell.is-hidden-by-filter{display:none}.reply-inlined{opacity:.5;text-decoration:underline dashed!important;text-underline-offset:2px}.quote-inlined{opacity:.5;text-decoration:underline dashed!important;text-underline-offset:2px}.target-highlight{background:var(--marked-color);border-color:var(--marked-border-color);color:var(--marked-text-color)}.statLabel{color:var(--link-color)}.statNumb{color:var(--text-color)}.postCell::before{display:inline!important;height:auto!important}.threadedReplies{border-left:1px solid #ccc;padding-left:15px}.ss-idlinks-floating{position:absolute;background:var(--background-color);color:var(--text-color);border:1px solid var(--navbar-text-color);padding:8px 15px 10px 10px;box-shadow:0 2px 12px rgba(0,0,0,.25);max-height:60vh;overflow-y:auto;font-size:14px;max-width:56vw;z-index:990}.ss-idlinks-floating .innerPost{margin-bottom:2px}";
  495. } else if (window.pageType?.isCatalog) {
  496. css += "#postingForm{margin:2em auto}#divTools>div:nth-child(5),#divTools>div:nth-child(6){float:left!important;margin-top:9px!important;margin-right:8px}";
  497. }
  498.  
  499. if (!css) return;
  500.  
  501. const style = document.createElement('style');
  502. style.id = '8chSS';
  503. style.textContent = css;
  504. document.head.appendChild(style);
  505. })();
  506. const faviconManager = (() => {
  507. const STYLES = [
  508. "default",
  509. "eight", "eight_dark",
  510. "pixel", "pixel_alt"
  511. ];
  512. const STATES = ["base", "unread", "notif"];
  513. const FAVICON_DATA = {
  514. default: {
  515. base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAq1BMVEUAAACKtMV2lqSE1PWZ1Oyr1eaqzNp7udPO7vuc3ffh8vmG0/OB0fKv3e+N0e3B4e693+56x+bb6e6YzeOgydl7wd3L3OOwytS35fjZ8fuQ2feB1Pbo9vyy5Pjm9Pmp4PW85fbW7feO1/S55Pbk8/mq3/TY7fbn9Pnf7/bQ6vWS1fDa7fWH0vCb0+rY6/Pc7POBzezA4/GNz+vJ4+662ebH1958wNzD6vqm4Ph4HvJJAAAAN3RSTlMADwXliF4dGP7949HLn5mUhWllVCglJBf6+fn5+Pjo4N3a2NfV1MzIxb28rayinJKOioB7UzEt0Y3/cQAAAIBJREFUGNOVjsUOhVAMRG97BXcePHd3Q/7/y0hhQWDHrDpJT86wIQHo1t/xHGLb8aNvzHnYFC2KFHh+mu1PNTW6GuVB0vlaJvTsTl2xWjgxw5upCOB3ZLEzXvve5IlE8DdJ5K7QL0nts61cEw9Q8p82kmDGt4ZgbTCwrS/0Rw9IBRlvB34XFuslAAAAAElFTkSuQmCC",
  516. unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAolBMVEUAAACG2vyC1/iH1vaB1/mC1PSC2PuC2fuD2fyE2fyG3P2I3Pqn3vWB1PWO1fKD2fuE2vqE2vyH3Puk4PaK1vSg2/OP1vJ/0fGB0/SF1vaC1faB1vmF2vyy5PiP2PbF6vnK6vjU7/m/5/ao3fKF0/K55PaW2PKK1PGA0/SP2PWA1/mC2fuE2fyD2fyx4vaT1e+S1/PX8PrO7vqb3fjh9Pu25fiNN6jgAAAAMXRSTlMAHK7Kmf6CYlc/EAnrxOt4OSwV/f3y8OrQv7afI/7+/f379/Pz8OLhvLmRb1pK+/PUqRXqEQAAALhJREFUGNN1j8cSgkAAQ7c3dykiSLFTpaiA/v+vCXrAi7klM5nkgUWrFfjVWgrhqMVDnkQREeuPURBCEevjSaNurrmcsYbo4dQ/NYMA7GxTlhinGj/OZ+QAxc1m9DwrRHad91cJXJIdgsDb+5igyxzImGYowf5r76d9TlwgK7qtbRRat+NwwVwBB1EaGutucFFE9m4aYVu6OYwBaRjj7kzQoSmxYjG9U1+IllQm4XDBUI5o5QTxB/wNI78Nh2SdMuEAAAAASUVORK5CYII=",
  517. notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA4VBMVEUAAACE3vuD1veC1veB2PqB2fuD2/2H2/2I3PqP1vOB1faE2vyN1/WD1PSB2PuE2vzQ7Pak4PaB0/OG0/J/0fGn3vSB1vmC2PqE2vmG2vq54vKf3fWrLzam4PaO1fGD1faP2PWE2fyD2fyD2fzW8PrD5PG11+WXtcW55PVvbnqqOj+XmKaT1e+x3e1+obS4AACBjqGH0/F8vNh5hpuQNj+P2PZyX2+C1/SKEBOC2ftjKjApAAAdAAAJAACD1fXkAADg9Pu05/qb3fi1vsefoq+xhIildHu4PECpMjSkLDDPAQHtACoGAAAAPnRSTlMAEsOumWJVGwnuuT39zIMs/f388erpn3k4I/Tz8/Lmz7laSkH++/v7+vr5+PPv7ezk4M7OzcSdkZFwWTIVCIgxa4YAAADDSURBVBjTdY/VrgJBGIP/cWZm3XE77oLLLm7v/0BAgMANvWuapv3gqlwOboU05wm6esEijClHp1AIwVX49BiSxrFmM8vq0/J8s5yElgDIx/i3Yprl9+zNcUgCiGFj5rqV6n9W+pzUmmBT35DSnRZ/slLhoaZBq9QnkVmcLrK1U6A2NFUadGNSlc/b1UubIdDE80wsP/Df6xeO84cRK0hTYyZpZ9di9pGgQQLP8BUffPfOP+tU4YiJ8XB0wUp4XR/CO+B76XMRs7uqu4cAAAAASUVORK5CYII=",
  518. },
  519. eight: {
  520. base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA6lBMVEUAAAAxV2oFSGADP1Y1Wm0zWW1EY3Q1VmgpU2g2VWYkUGUyUWIiTWEAKUcFSmErU2c5W24zVWghUWckTmIKSmEZTmUxUmMwUWP1+fxcc4HL6f7j9f/g7PWi1fWWprFJZXUeSl7Z7v6Myu7d4OOjt8SKrcOSrsFMa30/YnT////C5v/Q6PmHz/nt7/Gw1e+bzu+Dx+7A1+d7wOa+ztqtx9m5v8Sds8KDn7J0lqqJl6GEkZtJfZlRb4E5Zn1Ybnw0YHc3V2kPSWAAQVqY0vZpstqMu9mIutjO0tV5rcyru8dfm7xTmLthh51TfpcZngh2AAAAGHRSTlMA1cx8MBoH8fHm5s3Kv5qUiFhYUFDxmpptBtYcAAAAzUlEQVQY0zWOVRKDQBBEB4+77OLB4+5B43b/64QU8P76VddMQwpTyWGFbEAGXTjoaLxWqmlmSRnF9HUl7bSwKO49fO+vyURQsvCMiDZGY+Xfb1JlQ9iENcJDCMf3SqetOe8tgzDaIz0HTH43GfD8sLfcGCI6VKD7laamtYqNIIhygQHudj6qarDi54aMSRrA1jSfqKkWb5WpFhv/cBYzlyNUc7ClkgnF1+hh2x9pcmomou5cRtpUknYlFhKqznsxux7zNGTUi77LdZg0/QBWixhGoJiKLQAAAABJRU5ErkJggg==",
  521. unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAtFBMVEUAAAAAYYUAYYQAXIAAW38AYYMAY4YAXYAAYIQAZIcAY4Udc5MAWX8BZogldJQAWn6L2fIRa4xy0faB2/xt0vqI3Ph+0vBdxO1Wp8c/i6l41fhPwO1Ivu1Lo8dEoMYegaYYcZMEa46X5f1NxvZfyfNZyPOI1OxqwuV3w947rttputpdttldq8porscrmcNRnbtEmLpOmLU9krQFb5MHZIVIueY6tuaAyeJOstp1vNRCp9Auh6qJsLovAAAADnRSTlMAWfLRzujVm5EzHAqLiBrNc7IAAADDSURBVBjTNY9XEsIwEEPd0mFtp4d0eocU+v3vRRic9yeNZldCCtsgIBlGI5aWbMX6JE2lHdoEgRDzh1QZDGGYALzmJ/Y3JhfvyHUCYi1/eWzS2Iv5VAchYLhH4ZpvZvue8yTYEmRrbeT7rjvbx8cwSAxklMuoKA6D43lho9mI7FYl5/3B3cQXoBZCkKW1PuWFn7MJdoYf9T3riM5z/6pasvPiXVWfKKrGknK3SNPVsqWO2mHK8y17lpqFRjCrO2LYSn0BV4wQhK3VgFcAAAAASUVORK5CYII=",
  522. notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABEVBMVEUAAAAAW38AYoUAYoQAYYQAZYgAYYMAZIYAXoEBZom1AwbrAABGAAAALT4AAAAAYYRHAAAAAAAadJTeAAABZohHGymuAABFAACcAAAAAAAAAAAAUnIAZ4oAAAAQao2M2/Qic5Rw0/pKv+1EoscAXoD5AAB31fhfxO0egaYpdpUAa40AWX3/AgK/AAC6AABNAACX5f2B2/xr0fqH2/dNxvZy0fVfyfNZyPN80e+I1OxQv+x7xuA7rttmu9ldttlorsddqsdUpsZLocMrmcNEmLpPm7g+iqkFb5MZcJJYwu1IueY6tuZoweROstp1vNRdsdBCp9Bwt8xAk7U6kbRYnbEuh6oJZYWZdYM1RF0AKDwAHjhOZ3nrAAAAHnRSTlMA0FnyjRzo1psy/v785K2UiVwL+PLu6OPLyL6nm49luDk4AAAA4ElEQVQY0zWP5XLDMBAGT5YpnJRRkiHGOg0zc7lheP8HiTN29t/t7NzMByGCjBlBHFyIiNMWbfRIKryv0KRUojTfImHD2fX6lNm/+R4KBD8p9F0J27RBzj3Ho0FhoPOJw4Iy/x9i4+G30dH3UW+3vgFBXJlFRakY/ffsm3d8Admqfmhat2J0fJFLCIDbNcvVt13l7yyeAJjadCRe15RhPJaNJgHIlzrDkqsVx8/38TQAoFH5fz7fmKZzex27ewXgSLusNmvVJUrnvIeMn6TI6FP9scQIJB8zwRDkzLAsQMAJImsd3S869hIAAAAASUVORK5CYII=",
  523. },
  524. eight_dark: {
  525. base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA2FBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmKCkAAAADBAQAAAD1+fxMTEzM6v4vLy7i9f/E6P/v8PKMye2frbiFo7aNpLQ1P0b////Z7/+I0PrQ6Pmk1vab0/bi7PXe6fKfz+6w1O2Cxu15veS/1OPY2dq8ydOrwdKHtdK1traZqbSQmqF8kZ+QlJhshJSBhIZ7fH0+YnY9R09GREMOIiwfJivp+f/X7fyYzO3g4OBmrtTMzMt0pcKnsrpWjq1Ii6xUcIEyZX8ENUgYNkYAKD1NaNryAAAADnRSTlMAzPGY5tV8WFAwGge/iAmaI18AAADKSURBVBjTNY7XFoIwEEQ3CR1NQi/23rsC9q7//0fiAe7bzLlndyBHI5ibSIYCVdgMmTM1SZ51tGQplZOZOwpvt9YRv1SmKCukuXWjIubMMf++IpU8a0bLYuQwnt5Du4U/qI4opWs2xKAJq65tG/XDaOa12IYA2TY6fjA26tW91V4KGuBJ85XaY2PgzTlSAULX/YplGhhBSVL09Efc732wSH17IWUT0L12DcNnp7tTskJOzjX32GyskA4ZJHn0e5OtoEKBjOI3Jlqefl82Et7gg4sWAAAAAElFTkSuQmCC",
  526. unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA6lBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEAAADb3NxEQ0IAAAAAAABISEgAAAAAAAAlKCk9NzGurq6cnJwAAAD+/v73+Pi6u7tNTExGREMwLy7N6v61trY1QEfE6P/t7u+dzu6Mye2rwdKForaNpLTy+f/h9f/Z7//S6vuI0Pqk1vbg6/Sw1O2Cxu2+0uCHtdLKzM2qtbycqrV8kaCPl51shJV8f4I+YnYOIiwfJiud1PaY0vbo6Oh+vuR0vOPg4OBmrtS8ydN0pcLAwMBWjq1Ii6yjpaZUcIEyZX88R08ENUgYNkYAKD0sty6hAAAAFXRSTlMAzZjyWeUa1owv/Oe/roh8UAf26MtVcWhmAAAA10lEQVQY0zWP1ZLDMBRDr+04SbkLpjgMZeZ2mfn/f2ebqas3aTQaHTCyMVEaWXBWhW6rIg40Nv4CrTsdIfijNp22Go22nprzAJ2C1rq7YA5RItZlv92qL7tL5lI25+q4h75X+bT3zHQg5QcBm27GUeT3ewuPc8kw4N0wyYvM7089wSW1gcxuvxhjmZ+VQR0gTNM/x2WFX1zGPGgA7B8mv8RhebTCNa8JAOht8BKGn8k4rFXvrlwA6/A0SJOb4QY15et1+Rwf3u8nsx2tQMPQWmj/Q7BtOP8BG/MW/0ZJlAQAAAAASUVORK5CYII=",
  527. notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABDlBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAC2AQHrAAA4AAAAAAAAAABIAgIAAAAAAAAlKCneAABDAACuAABFAACcAAADBAQAAAAAAAC8AABNTE3+AQHN6v72+PmKo7QvLy5GAAD+/v/E6P+Myu6rwtLy+f/h9f/Z7//S6vuI0Pqk1vab0/bg6/Sfz+6w1O2Cxu2+0uHc3d2HtdLKzc61tracqrV8kaBshJV8f4I+YnY5R081QUkOIiwfJiv3AADt7u/r7e6YzO1+vuR0vONmrtS8ydN0pcKtucGnsrqQqLeCordWjq1Ii6yjqaqQmqGOk5i3hYhUcIEyZX8ENUgYNkZGREM1PEEAKD1ovUZyAAAAGnRSTlMAzfLlmliSGtYv/v77v66IfFAH+Ozo4suIXxNuy1MAAADcSURBVBjTNY9VjkJBFERvyxMcxud2Pxfc3cfdffa/ESA09VeVk0oOqBicSo9osEuCTbrO+cDTVd8j78Wi4wRdTzEF2WhMfmU/GJDtkH8rPaBJpXPhbfhCPjMsDVFn2A/k+o/MRm6nfIvppPh4pWCwcSsM7Ur53rcsgRz4tNZ8cmO70vHPLMEMoL36FyLGdrwZMgDzKPo3T9C1H9MpK5kFWFy3f6iJbjjiR34OAMhz9W7+99lszQ72U4c6gLa8qUaX9dqY5IQ4PgUAffly1e5NWQKyylYji2/KDeW5AobCG0Lco88iAAAAAElFTkSuQmCC",
  528. },
  529. pixel: {
  530. base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAAAAAAAABiam00WmxcZWlFTE4jQE1GTU8jQU4yV2g8QUIbNkE/WGRWYWY9WGUAAAAAAADg8vnY6fB5yOyA0fW95fi23fB+z/PQ4Od0weNoeYFWcoBSXmNFS00jP0wFxP2oAAAAEnRSTlMAIRHe3v3w8Nzc/ezs/u7u7EbfERAsAAAAcUlEQVQY05VP1wqAMAw0de82TYf7//9SOhBFELy3G1wuyQcYYw8OVVHBzYJUokzBW3VZg+MYFMhI0ThMMm9zlIXnVmmzbFy0iHtSkmq63mhtYuJQthO8X40OHSHR0DwO4UrsoAziDq84HudcO15P/MAJxpAGt0Xg7i0AAAAASUVORK5CYII=",
  531. unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAATHcAIGJii6I0gKFFepYjdJVGepYjdJUAXIFeiaEyf6A8dZIbcJJThZ8/f55WhZ89f58AY4YAM2ng8/p/0vbY6/R5zPC95vq23/TQ5O10xelolKxWj6tFepUjdJUJarSxAAAAFHRSTlMAIBHe3vDw3Nwj/f3s7P7+7u7sRkWW9w4AAAB4SURBVBjTlY/ZDoUwCERL3fcNSnsX/f+/tEpjNCYm8nZgMjOoh9FaXxiyJIPTCSKDJoL9lKc5bIx+U3iOmXjoP6asSzSJKjwTWfdrx6lGnFXKVDWds9a1oliImmns/s6Khygq/g69pAQPjiH0kBTPoc7R4/bEi1kBHCgHTIPhydUAAAAASUVORK5CYII=",
  532. notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAhFBMVEUAAAAAV34AIDg0gKEkdJQjdJUUAAMAAEAyf6AbcJI7dZA/f55MAABAgaBEeZVljaVUhJ4AZIZpkKYAQmp5y/B+0fVrS0224PPf8vnP4uu85vqA0/fY6/NUIivtAABNHynjAAB0xelolKxWj6tNgZpFepUjdJVOTU9fREtAREvaAABiAADosgfnAAAAFHRSTlMAIBHe8NzcIP3s3v7x7u7e7uzeRqhOt1sAAACESURBVBjTlY9ZDoMwDERxCHvp6mASSKH7dv/71YWoAiEh4b83Ho3H3swIIUYchaswGqwgta1N4ccQyABgr4tjoTfA7KPCfKtrFsp63bGqDJ2vloXy5klUcZKRMfRsO8e9Uslhl12IXp8HZ/SOGE+5bN4NX3EZ6IPr0SvMrs6/x+SJBfMF/doItAPX5RIAAAAASUVORK5CYII=",
  533. },
  534. pixel_alt: {
  535. base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAY1BMVEUAAAAATHcAIGJii6I0gKFFepYjdJVGepYjdJUAXIFeiaEyf6A8dZIbcJJThZ8/f55WhZ89f58AY4YAQmoAJGjg8/p/0vbY6/R5zPC95vq23/TQ5O10xelolKxWj6tFepUjdJUOfB3LAAAAFXRSTlMAIBHe3vDw3Nwj/f3s7P7+7u7sRkaqQLDYAAAAeUlEQVQY05WP6Q6EMAiES73vYxdKu4e+/1NapTEaExP598FkZlA3o7U+MWRJBocTRAZNBNspT3NYGf2m8Bwz8dB/TFmXaBJVeCay7teO7xpxUilT1XTOWteKYiZqXmP3d1Y8RFHxd+glJXhwDKGHpHgOdfYelycezAI9gweHH0yPzQAAAABJRU5ErkJggg==",
  536. unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAhFBMVEUAAAAjR1sKChFJeZBBa4FAZ3tCbIEVFRYABA9IeI9OaXlReI1MTExSeo9WcYBxh5RjfYw4WGt0ipcdKTh7y+6A0vdqbm/f8fi85fl+0PTY6vK23fDt7OzP4OdVWFlOUlS44PPj4+NZdITR4+t3xefa2tpyj55jiZ1Ba4BgZ2tiYF9ATFKjqzLCAAAAFHRSTlMAIBHe8O3c3CD93v7x7u7e7uzeRvBdAgMAAACFSURBVBjTlY9ZDoMwDERxIKylqwOBlATo3t7/fjVpVIGQkPDfG4/GY29hGGMTjqNNFI9WkOpOpzAwhEEIcGyKc9HsgNhHgfn+UpNQ1lvLlZDtvdcklL0XoEh4pqRUr846nqLip0P2aNXnbSjj50jwmnNzM3TFZaAProdVBnZ1/j1mT6yYLxHCCNKMC/PDAAAAAElFTkSuQmCC",
  537. notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAdVBMVEUAAAAkSFwKChFAaX9JeZAABBhCbIEVAABIeI9yiJVObnxReI1MAABSeo9WcYBjfYw4WGsdKTiA0vd7y+5rS0233/JSHiPf8vnP4um85fnY6/LtAABZdobjAAB3xedyj55jiZ1Ba4BOTU9gREtAREvaAABiAADFYUIqAAAAEnRSTlMAIRHv3iDc3P3e3v7x7u7u7EYDRb2ZAAAAgklEQVQY05WPWQ7CMAxE67jpSlnsOk1Kyw73PyJJFCEQUqX6741H43G2MEqpH9blptRfK2jMbBoIDAUWoGvbH3tbg+ecibudHb0wjNvIRE7Ok/HCMGXIVGErzsl9jo4rER727UXk8bqFjOio+NSheYYrKYNzSD2iEjjV+fT4e2LFvAFuzge5hY5wYgAAAABJRU5ErkJggg==",
  538. }
  539. };
  540. let currentStyle = "default";
  541. let currentState = "base";
  542. let cachedUserStyle = null;
  543. function removeFavicons() {
  544. const head = document.head;
  545. if (!head) return;
  546. head.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]').forEach(link => link.remove());
  547. }
  548. function insertFavicon(href) {
  549. const head = document.head;
  550. if (!head) return;
  551. const link = document.createElement('link');
  552. link.rel = 'icon';
  553. link.type = 'image/png';
  554. link.href = href;
  555. head.appendChild(link);
  556. }
  557. async function getUserFaviconStyle() {
  558. if (cachedUserStyle) return cachedUserStyle;
  559. let style = "default";
  560. try {
  561. style = await getSetting("customFavicon_faviconStyle");
  562. } catch { }
  563. if (!STYLES.includes(style)) style = "default";
  564. cachedUserStyle = style;
  565. return style;
  566. }
  567. async function setFaviconStyle(style, state = "base") {
  568. if (!STYLES.includes(style)) style = "default";
  569. if (!STATES.includes(state)) state = "base";
  570. if (currentStyle === style && currentState === state) return;
  571.  
  572. const url = (FAVICON_DATA?.[style]?.[state]) || FAVICON_DATA.default.base;
  573. removeFavicons();
  574. insertFavicon(url);
  575. currentStyle = style;
  576. currentState = state;
  577. document.dispatchEvent(new CustomEvent("faviconStateChanged", {
  578. detail: { style, state }
  579. }));
  580. }
  581. async function setFavicon(state = "base") {
  582. if (!STATES.includes(state)) state = "base";
  583. const style = await getUserFaviconStyle();
  584. await setFaviconStyle(style, state);
  585. }
  586. async function resetFavicon() {
  587. await setFavicon("base");
  588. }
  589. function getCurrentFaviconState() {
  590. return { style: currentStyle, state: currentState };
  591. }
  592.  
  593. return {
  594. setFavicon,
  595. setFaviconStyle,
  596. resetFavicon,
  597. getCurrentFaviconState,
  598. STYLES,
  599. STATES
  600. };
  601. })();
  602. const featureMap = [
  603. { key: "enableScrollSave", fn: featureSaveScroll },
  604. { key: "watchThreadOnReply", fn: featureWatchThreadOnReply },
  605. { key: "blurSpoilers", fn: featureBlurSpoilers },
  606. { key: "enableHeaderCatalogLinks", fn: featureHeaderCatalogLinks },
  607. { key: "openCatalogThreadNewTab", fn: catalogThreadsInNewTab },
  608. { key: "enableScrollArrows", fn: featureScrollArrows },
  609. { key: "alwaysShowTW", fn: featureAlwaysShowTW },
  610. { key: "scrollToBottom", fn: preventFooterScrollIntoView },
  611. { key: "enableThreadHiding", fn: featureCatalogHiding },
  612. { key: "switchTimeFormat", fn: featureLabelCreated12h },
  613. { key: "enableIdFilters", fn: enableIdFiltering },
  614. { key: "enhanceYoutube", fn: enhanceYouTubeLinks },
  615. { key: "threadStatsInHeader", fn: threadInfoHeader },
  616. { key: "enableHashNav", fn: hashNavigation },
  617. { key: "hideAnnouncement", fn: featureHideAnnouncement },
  618. { key: "enableAutoHideHeaderScroll", fn: autoHideHeaderOnScroll },
  619. { key: "enableMediaViewer", fn: mediaViewerPositioning },
  620. { key: "customFavicon", fn: enableFavicon },
  621. { key: "highlightNewIds", fn: featureHighlightNewIds },
  622. { key: "quoteThreading", fn: featureQuoteThreading },
  623. { key: "enableLastFifty", fn: featureLastFifty },
  624. { key: "enableIdToggle", fn: featureToggleIdAsYours },
  625. { key: "enableTheSauce", fn: featureSauceLinks },
  626. { key: "enableUpdateNotif", fn: updateNotif },
  627. { key: "enableHidingMenu", fn: featureCustomPostHideMenu },
  628. { key: "alwaysShowIdCount", fn: featureShowIDCount },
  629. ];
  630. for (const { key, fn } of featureMap) {
  631. try {
  632. if (await getSetting(key)) {
  633. fn();
  634. }
  635. } catch (e) {
  636. console.error(`${fn.name || 'Feature'} failed:`, e);
  637. }
  638. }
  639. if (await getSetting("truncFilenames")) {
  640. try {
  641. const filenameLength = await getSetting("truncFilenames_customTrunc");
  642. truncateFilenames(filenameLength);
  643. } catch (e) {
  644. console.error("truncateFilenames failed:", e);
  645. }
  646. }
  647. async function enableFavicon() {
  648. try {
  649. const customFaviconEnabled = await getSetting("customFavicon");
  650. const selectedStyle = await getSetting("customFavicon_faviconStyle");
  651.  
  652. if (customFaviconEnabled) {
  653. if (selectedStyle && typeof selectedStyle === 'string') {
  654. await faviconManager.setFaviconStyle(selectedStyle);
  655. } else {
  656. console.warn("Invalid favicon style:", selectedStyle);
  657. await faviconManager.setFaviconStyle("eight_dark");
  658. }
  659. } else {
  660. await faviconManager.resetFavicon();
  661. }
  662. } catch (e) {
  663. console.error("Error updating favicon:", e);
  664. }
  665. }
  666. let imageHoverEnabled = false;
  667. try {
  668. if (window.pageType?.isCatalog) {
  669. imageHoverEnabled = await getSetting("enableCatalogImageHover");
  670. } else {
  671. imageHoverEnabled = await getSetting("enableThreadImageHover");
  672. }
  673. if (imageHoverEnabled) {
  674. localStorage.removeItem("hoveringImage");
  675. featureImageHover();
  676. }
  677. } catch (e) {
  678. console.error("featureImageHover failed:", e);
  679. }
  680. (function () {
  681. function sanitizeToastHTML(html) {
  682. html = html.replace(/<(\/?)(?!a\b|b\b|i\b|u\b|strong\b|em\b)[^>]*>/gi, '');
  683. html = html.replace(/<(b|i|u|strong|em)[^>]*>/gi, '<$1>');
  684. html = html.replace(/<a\s+([^>]+)>/gi, function (match, attrs) {
  685. let allowed = '';
  686. attrs.replace(/(\w+)\s*=\s*(['"])(.*?)\2/gi, function (_, name, q, value) {
  687. name = name.toLowerCase();
  688. if (['href', 'target', 'rel'].includes(name)) {
  689. if (name === 'href' && (/^\s*javascript:/i.test(value) || /^\s*data:/i.test(value))) return;
  690. allowed += ` ${name}=${q}${value}${q}`;
  691. }
  692. });
  693. return `<a${allowed}>`;
  694. });
  695.  
  696. return html;
  697. }
  698.  
  699. const script = document.createElement('script');
  700. script.textContent = '(' + function (sanitizeToastHTML) {
  701. window.showGlobalToast = function (htmlMessage, color = "black", duration = 1200) {
  702. if (document.querySelector('.global-toast-notification')) {
  703. return;
  704. }
  705.  
  706. const colorMap = {
  707. black: "#222",
  708. orange: "#cc7a00",
  709. green: "#339933",
  710. blue: "#1976d2",
  711. red: "#c62828"
  712. };
  713. const bgColor = colorMap[color] || color;
  714.  
  715. const icon = document.getElementById("8chanSS-icon");
  716. let toast = document.createElement("span");
  717. toast.className = "global-toast-notification";
  718. toast.innerHTML = sanitizeToastHTML(htmlMessage);
  719. toast.style.position = "absolute";
  720. toast.style.background = bgColor;
  721. toast.style.color = "#fff";
  722. toast.style.padding = "2px 12px";
  723. toast.style.borderRadius = "4px";
  724. toast.style.fontSize = "13px";
  725. toast.style.zIndex = 99999;
  726. toast.style.opacity = "1";
  727. toast.style.transition = "opacity 0.3s";
  728. toast.style.pointerEvents = "auto";
  729. toast.style.boxShadow = "0 2px 8px rgba(0,0,0,0.18)";
  730. let closeBtn = document.createElement("span");
  731. closeBtn.textContent = "✕";
  732. closeBtn.style.marginLeft = "10px";
  733. closeBtn.style.cursor = "pointer";
  734. closeBtn.style.fontWeight = "bold";
  735. closeBtn.style.fontSize = "15px";
  736. closeBtn.style.opacity = "0.7";
  737. closeBtn.style.float = "right";
  738. closeBtn.style.userSelect = "none";
  739. closeBtn.onclick = function (e) {
  740. e.stopPropagation();
  741. if (toast.parentNode) toast.parentNode.removeChild(toast);
  742. if (timeout1) clearTimeout(timeout1);
  743. if (timeout2) clearTimeout(timeout2);
  744. };
  745. closeBtn.onmouseover = function () { closeBtn.style.opacity = "1"; };
  746. closeBtn.onmouseout = function () { closeBtn.style.opacity = "0.7"; };
  747. toast.appendChild(closeBtn);
  748.  
  749. if (icon && icon.parentNode) {
  750. toast.style.left = (icon.offsetLeft - 50) + "px";
  751. toast.style.top = "28px";
  752. icon.parentNode.appendChild(toast);
  753. } else {
  754. toast.style.right = "25px";
  755. toast.style.top = "25px";
  756. toast.style.position = "fixed";
  757. document.body.appendChild(toast);
  758. }
  759.  
  760. let timeout1 = setTimeout(() => { toast.style.opacity = "0"; }, duration - 300);
  761. let timeout2 = setTimeout(() => { toast.remove(); }, duration);
  762. };
  763. } + ')(' + sanitizeToastHTML.toString() + ');';
  764. document.documentElement.appendChild(script);
  765. script.remove();
  766. window.callPageToast = function (msg, color = 'black', duration = 1200) {
  767. const script = document.createElement('script');
  768. script.textContent = `window.showGlobalToast && window.showGlobalToast(${JSON.stringify(msg)}, ${JSON.stringify(color)}, ${duration});`;
  769. document.documentElement.appendChild(script);
  770. script.remove();
  771. };
  772. })();
  773. async function featureSaveScroll() {
  774. if (!window.pageType?.isThread) return;
  775.  
  776. const STORAGE_KEY = "8chanSS_scrollPositions";
  777. const UNREAD_LINE_ID = "unread-line";
  778. const MAX_THREADS = 200;
  779. function getBoardAndThread() {
  780. const match = window.location.pathname.match(/^\/([^/]+)\/res\/([^/.]+)\.html$/i);
  781. if (!match) return null;
  782. return { board: match[1], thread: match[2] };
  783. }
  784. async function getAllSavedScrollData() {
  785. const saved = await GM.getValue(STORAGE_KEY, null);
  786. if (!saved) return {};
  787. try { return JSON.parse(saved); } catch { return {}; }
  788. }
  789. async function setAllSavedScrollData(data) {
  790. await GM.setValue(STORAGE_KEY, JSON.stringify(data));
  791. }
  792. function getCurrentPostCount() {
  793. if (!divPosts) return 0;
  794. return divPosts.querySelectorAll(":scope > .postCell[id]").length;
  795. }
  796. function removeUnreadLineMarker() {
  797. const oldMarker = document.getElementById(UNREAD_LINE_ID);
  798. if (oldMarker && oldMarker.parentNode) {
  799. oldMarker.parentNode.removeChild(oldMarker);
  800. }
  801. }
  802. let lastSeenPostCount = 0;
  803. let unseenCount = 0;
  804. let tabTitleBase = null;
  805. let previousFaviconState = null;
  806. const customFaviconEnabled = await getSetting("customFavicon");
  807.  
  808. async function updateTabTitle() {
  809. if (window.isNotifying) return;
  810. if (!tabTitleBase) tabTitleBase = document.title.replace(/^\(\d+\)\s*/, "");
  811. document.title = unseenCount > 0 ? `(${unseenCount}) ${tabTitleBase}` : tabTitleBase;
  812. const { style, state } = faviconManager.getCurrentFaviconState();
  813.  
  814. if (unseenCount > 0 && customFaviconEnabled) {
  815. if (state !== "unread") {
  816. previousFaviconState = { style, state };
  817. }
  818. faviconManager.setFaviconStyle(style, "unread");
  819. } else if (unseenCount == 0 && customFaviconEnabled) {
  820. if (state === "unread" && previousFaviconState) {
  821. faviconManager.setFaviconStyle(previousFaviconState.style, previousFaviconState.state);
  822. previousFaviconState = null;
  823. } else if (state === "unread") {
  824. faviconManager.setFavicon("base");
  825. }
  826. }
  827. }
  828. async function updateUnseenCountFromSaved() {
  829. const info = getBoardAndThread();
  830. if (!info) return;
  831. const allData = await getAllSavedScrollData();
  832. const key = `${info.board}/${info.thread}`;
  833. const saved = allData[key];
  834. const currentCount = getCurrentPostCount();
  835. lastSeenPostCount = (saved && typeof saved.lastSeenPostCount === "number") ? saved.lastSeenPostCount : 0;
  836. unseenCount = Math.max(0, currentCount - lastSeenPostCount);
  837. updateTabTitle();
  838. }
  839. let lastScrollY = window.scrollY;
  840. async function onScrollUpdateSeen() {
  841. const info = getBoardAndThread();
  842. if (!info || !(await getSetting("enableScrollSave"))) return;
  843. const posts = Array.from(document.querySelectorAll(".divPosts > .postCell[id]"));
  844. let maxIndex = -1;
  845. for (let i = 0; i < posts.length; ++i) {
  846. const rect = posts[i].getBoundingClientRect();
  847. if (rect.bottom > 0 && rect.top < window.innerHeight) maxIndex = i;
  848. }
  849. const currentCount = getCurrentPostCount();
  850. let newLastSeen = lastSeenPostCount;
  851.  
  852. if (window.scrollY > lastScrollY) {
  853. if (maxIndex >= 0 && currentCount > 0) {
  854. if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 20)) {
  855. newLastSeen = currentCount;
  856. } else {
  857. newLastSeen = Math.max(lastSeenPostCount, maxIndex + 1);
  858. }
  859. }
  860. if (newLastSeen !== lastSeenPostCount) {
  861. lastSeenPostCount = newLastSeen;
  862. let allData = await getAllSavedScrollData();
  863. const key = `${info.board}/${info.thread}`;
  864. if (!allData[key]) allData[key] = {};
  865. allData[key].lastSeenPostCount = lastSeenPostCount;
  866. allData[key].timestamp = Date.now();
  867. if (
  868. typeof allData[key].position !== "number" ||
  869. window.scrollY > allData[key].position
  870. ) {
  871. allData[key].position = window.scrollY;
  872. }
  873. await setAllSavedScrollData(allData);
  874. }
  875. unseenCount = Math.max(0, currentCount - lastSeenPostCount);
  876. updateTabTitle();
  877. }
  878. lastScrollY = window.scrollY;
  879. }
  880. async function saveScrollPosition() {
  881. const info = getBoardAndThread();
  882. if (!info || !(await getSetting("enableScrollSave"))) return;
  883.  
  884. const scrollPosition = window.scrollY;
  885. const timestamp = Date.now();
  886.  
  887. let allData = await getAllSavedScrollData();
  888. const keys = Object.keys(allData);
  889. if (keys.length >= MAX_THREADS) {
  890. keys.sort((a, b) => (allData[a].timestamp || 0) - (allData[b].timestamp || 0));
  891. for (let i = 0; i < keys.length - MAX_THREADS + 1; ++i) delete allData[keys[i]];
  892. }
  893.  
  894. const key = `${info.board}/${info.thread}`;
  895. if (!allData[key]) allData[key] = {};
  896. if (
  897. typeof allData[key].position !== "number" ||
  898. scrollPosition > allData[key].position
  899. ) {
  900. allData[key].position = scrollPosition;
  901. allData[key].timestamp = timestamp;
  902. await setAllSavedScrollData(allData);
  903. }
  904. }
  905. function scrollElementToViewportCenter(el) {
  906. if (!el) return;
  907. const rect = el.getBoundingClientRect();
  908. const elTop = rect.top + window.pageYOffset;
  909. const elHeight = rect.height;
  910. const viewportHeight = window.innerHeight;
  911. const scrollTo = elTop - (viewportHeight / 2) + (elHeight / 2);
  912. window.scrollTo({ top: scrollTo, behavior: "auto" });
  913. }
  914. async function restoreScrollPosition() {
  915. const info = getBoardAndThread();
  916. if (!info || !(await getSetting("enableScrollSave"))) return;
  917.  
  918. const allData = await getAllSavedScrollData();
  919. const key = `${info.board}/${info.thread}`;
  920. const saved = allData[key];
  921. if (!saved || typeof saved.position !== "number") return;
  922.  
  923. const anchor = window.location.hash ? window.location.hash.replace(/^#/, "") : null;
  924. const safeAnchor = anchor && /^[a-zA-Z0-9_-]+$/.test(anchor) ? anchor : null;
  925.  
  926. if (safeAnchor) {
  927. setTimeout(() => {
  928. const post = document.getElementById(safeAnchor);
  929. if (post && post.classList.contains("postCell")) {
  930. scrollElementToViewportCenter(post);
  931. }
  932. addUnreadLineAtSavedScrollPosition(saved.position, false);
  933. }, 25);
  934. return;
  935. }
  936. saved.timestamp = Date.now();
  937. await setAllSavedScrollData(allData);
  938. window.scrollTo({ top: saved.position, behavior: "auto" });
  939. setTimeout(() => addUnreadLineAtSavedScrollPosition(saved.position, false), 80);
  940. }
  941. async function addUnreadLineAtSavedScrollPosition(scrollPosition, centerAfter = false) {
  942. if (!(await getSetting("enableScrollSave_showUnreadLine"))) return;
  943. if (!divPosts) return;
  944. const margin = 5;
  945. const docHeight = document.body.offsetHeight;
  946. if ((scrollPosition + window.innerHeight) >= (docHeight - margin)) {
  947. return;
  948. }
  949. const posts = Array.from(divPosts.querySelectorAll(":scope > .postCell[id]"));
  950. let targetPost = null;
  951. for (let i = 0; i < posts.length; ++i) {
  952. const postTop = posts[i].offsetTop;
  953. if (postTop > scrollPosition) break;
  954. targetPost = posts[i];
  955. }
  956. if (!targetPost) return;
  957. removeUnreadLineMarker();
  958. const marker = document.createElement("hr");
  959. marker.id = UNREAD_LINE_ID;
  960. if (targetPost.nextSibling) {
  961. divPosts.insertBefore(marker, targetPost.nextSibling);
  962. } else {
  963. divPosts.appendChild(marker);
  964. }
  965. if (centerAfter) {
  966. setTimeout(() => {
  967. const markerElem = document.getElementById(UNREAD_LINE_ID);
  968. if (markerElem) {
  969. const rect = markerElem.getBoundingClientRect();
  970. const desiredY = window.innerHeight / 3;
  971. const scrollY = window.scrollY + rect.top - desiredY;
  972. window.scrollTo({ top: scrollY, behavior: "auto" });
  973. }
  974. }, 25);
  975. }
  976. }
  977. let unseenUpdateTimeout = null;
  978. function debouncedUpdateUnseenCount() {
  979. if (unseenUpdateTimeout) clearTimeout(unseenUpdateTimeout);
  980. unseenUpdateTimeout = setTimeout(() => {
  981. updateUnseenCountFromSaved();
  982. unseenUpdateTimeout = null;
  983. }, 100);
  984. }
  985.  
  986. const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: false });
  987. if (divPostsObs) {
  988. divPostsObs.addHandler(function saveScrollPostCountHandler() {
  989. debouncedUpdateUnseenCount();
  990. });
  991. }
  992. async function removeUnreadLineIfAtBottom() {
  993. if (!(await getSetting("enableScrollSave_showUnreadLine"))) return;
  994. const margin = 5;
  995. if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - margin)) {
  996. removeUnreadLineMarker();
  997. }
  998. }
  999. window.addEventListener("beforeunload", () => {
  1000. saveScrollPosition();
  1001. });
  1002.  
  1003. document.addEventListener("DOMContentLoaded", () => {
  1004. tabTitleBase = document.title.replace(/^\(\d+\)\s*/, "");
  1005. updateTabTitle();
  1006. });
  1007. window.addEventListener("load", async () => {
  1008. await restoreScrollPosition();
  1009. await updateUnseenCountFromSaved();
  1010. });
  1011.  
  1012. let scrollTimeout = null;
  1013. window.addEventListener("scroll", () => {
  1014. if (scrollTimeout) return;
  1015. scrollTimeout = setTimeout(async () => {
  1016. await onScrollUpdateSeen();
  1017. await removeUnreadLineIfAtBottom();
  1018. scrollTimeout = null;
  1019. }, 100);
  1020. });
  1021. await restoreScrollPosition();
  1022. await updateUnseenCountFromSaved();
  1023. }
  1024. async function featureHeaderCatalogLinks() {
  1025. async function appendCatalogToLinks() {
  1026. const navboardsSpan = document.getElementById("navBoardsSpan");
  1027. if (navboardsSpan) {
  1028. const links = navboardsSpan.getElementsByTagName("a");
  1029. const openInNewTab = await getSetting("enableHeaderCatalogLinks_openInNewTab");
  1030.  
  1031. for (let link of links) {
  1032. if (
  1033. link.href &&
  1034. !link.href.endsWith("/catalog.html") &&
  1035. !link.dataset.catalogLinkProcessed
  1036. ) {
  1037. link.href += "/catalog.html";
  1038. link.dataset.catalogLinkProcessed = "1";
  1039. if (openInNewTab) {
  1040. link.target = "_blank";
  1041. link.rel = "noopener noreferrer";
  1042. } else {
  1043. link.target = "";
  1044. link.rel = "";
  1045. }
  1046. }
  1047. }
  1048. }
  1049. }
  1050. appendCatalogToLinks();
  1051. const debouncedAppend = debounce(appendCatalogToLinks, 100);
  1052. const navboardsObs = observeSelector('#navBoardsSpan', { childList: true, subtree: true });
  1053. if (navboardsObs) {
  1054. navboardsObs.addHandler(function headerCatalogLinksHandler() {
  1055. debouncedAppend();
  1056. });
  1057. }
  1058. }
  1059. function catalogThreadsInNewTab() {
  1060. if (!window.pageType?.isCatalog) return;
  1061. catalogDiv.querySelectorAll('.catalogCell a.linkThumb').forEach(link => {
  1062. if (link.getAttribute('target') !== '_blank') {
  1063. link.setAttribute('target', '_blank');
  1064. }
  1065. });
  1066. catalogDiv.addEventListener('click', function (e) {
  1067. const link = e.target.closest('.catalogCell a.linkThumb');
  1068. if (link && link.getAttribute('target') !== '_blank') {
  1069. link.setAttribute('target', '_blank');
  1070. }
  1071. });
  1072. }
  1073. function featureImageHover() {
  1074. const MEDIA_MAX_WIDTH = "90vw";
  1075. const MEDIA_OPACITY_LOADING = "0";
  1076. const MEDIA_OPACITY_LOADED = "1";
  1077. const MEDIA_OFFSET = 50;
  1078. const MEDIA_BOTTOM_MARGIN = 3;
  1079. const AUDIO_INDICATOR_TEXT = "▶ Playing audio...";
  1080. function getExtensionForMimeType(mime) {
  1081. const map = {
  1082. "image/jpeg": ".jpg",
  1083. "image/jpg": ".jpg",
  1084. "image/jxl": ".jxl",
  1085. "image/png": ".png",
  1086. "image/apng": ".png",
  1087. "image/gif": ".gif",
  1088. "image/avif": ".avif",
  1089. "image/webp": ".webp",
  1090. "image/bmp": ".bmp",
  1091. "video/mp4": ".mp4",
  1092. "video/webm": ".webm",
  1093. "video/x-m4v": ".m4v",
  1094. "audio/ogg": ".ogg",
  1095. "audio/mpeg": ".mp3",
  1096. "audio/x-m4a": ".m4a",
  1097. "audio/x-wav": ".wav",
  1098. };
  1099. return map[mime.toLowerCase()] || null;
  1100. }
  1101. function sanitizeUrl(url) {
  1102. try {
  1103. const parsed = new URL(url, window.location.origin);
  1104. if ((parsed.protocol === "http:" || parsed.protocol === "https:") &&
  1105. parsed.origin === window.location.origin) {
  1106. return parsed.href;
  1107. }
  1108. } catch { }
  1109. return "";
  1110. }
  1111. let floatingMedia = null;
  1112. let cleanupFns = [];
  1113. let currentAudioIndicator = null;
  1114. let lastMouseEvent = null;
  1115. function clamp(val, min, max) {
  1116. return Math.max(min, Math.min(max, val));
  1117. }
  1118. function positionFloatingMedia(event) {
  1119. if (!floatingMedia) return;
  1120. const vw = window.innerWidth;
  1121. const vh = window.innerHeight;
  1122. const mw = floatingMedia.offsetWidth || 0;
  1123. const mh = floatingMedia.offsetHeight || 0;
  1124.  
  1125. const docElement = document.documentElement;
  1126. const SCROLLBAR_WIDTH = window.innerWidth - docElement.clientWidth;
  1127. const MEDIA_BOTTOM_MARGIN_PX = vh * (MEDIA_BOTTOM_MARGIN / 100);
  1128.  
  1129. let x, y;
  1130. const rightX = event.clientX + MEDIA_OFFSET;
  1131. const leftX = event.clientX - MEDIA_OFFSET - mw;
  1132. if (rightX + mw <= vw - SCROLLBAR_WIDTH) {
  1133. x = rightX;
  1134. }
  1135. else if (leftX >= 0) {
  1136. x = leftX;
  1137. }
  1138. else {
  1139. x = clamp(rightX, 0, vw - mw - SCROLLBAR_WIDTH);
  1140. }
  1141.  
  1142. y = event.clientY;
  1143. const maxY = vh - mh - MEDIA_BOTTOM_MARGIN_PX;
  1144. y = Math.max(0, Math.min(y, maxY));
  1145.  
  1146. floatingMedia.style.left = `${x}px`;
  1147. floatingMedia.style.top = `${y}px`;
  1148. }
  1149. function cleanupFloatingMedia() {
  1150. cleanupFns.forEach(fn => { try { fn(); } catch { } });
  1151. cleanupFns = [];
  1152. if (floatingMedia) {
  1153. if (["VIDEO", "AUDIO"].includes(floatingMedia.tagName)) {
  1154. try {
  1155. floatingMedia.pause();
  1156. floatingMedia.srcObject = null;
  1157. URL.revokeObjectURL(floatingMedia.src);
  1158. floatingMedia.removeAttribute("src");
  1159. floatingMedia.load();
  1160. } catch { }
  1161. }
  1162. floatingMedia.remove();
  1163. floatingMedia = null;
  1164. }
  1165. if (currentAudioIndicator && currentAudioIndicator.parentNode) {
  1166. currentAudioIndicator.parentNode.removeChild(currentAudioIndicator);
  1167. currentAudioIndicator = null;
  1168. }
  1169. }
  1170. function getFullMediaSrc(thumbNode, filemime) {
  1171. const thumbnailSrc = thumbNode.getAttribute("src");
  1172. const parentA = thumbNode.closest("a.linkThumb, a.imgLink");
  1173. const href = parentA ? parentA.getAttribute("href") : "";
  1174. const fileWidth = parentA ? parseInt(parentA.getAttribute("data-filewidth"), 10) : null;
  1175. const fileHeight = parentA ? parseInt(parentA.getAttribute("data-fileheight"), 10) : null;
  1176. function hasExtension(str) {
  1177. return /\.[a-z0-9]+$/i.test(str);
  1178. }
  1179. function isTThumb(str) {
  1180. return /\/t_/.test(str);
  1181. }
  1182. function isDirectHash(str) {
  1183. return /^\/\.media\/[a-f0-9]{40,}$/i.test(str) && !hasExtension(str);
  1184. }
  1185. function isSmallImage() {
  1186. return (fileWidth && fileWidth <= 220) || (fileHeight && fileHeight <= 220);
  1187. }
  1188. function isBarePngNoThumb() {
  1189. return (
  1190. filemime &&
  1191. filemime.toLowerCase() === "image/png" &&
  1192. parentA &&
  1193. !isTThumb(href) &&
  1194. !hasExtension(href)
  1195. );
  1196. }
  1197. function isSmallBarePngSrc() {
  1198. return (
  1199. isSmallImage() &&
  1200. filemime &&
  1201. filemime.toLowerCase() === "image/png" &&
  1202. !isTThumb(thumbnailSrc) &&
  1203. !hasExtension(thumbnailSrc)
  1204. );
  1205. }
  1206. function isGenericThumb() {
  1207. return (
  1208. /\/spoiler\.png$/i.test(thumbnailSrc) ||
  1209. /\/custom\.spoiler$/i.test(thumbnailSrc) ||
  1210. /\/audioGenericThumb\.png$/i.test(thumbnailSrc)
  1211. );
  1212. }
  1213. if (!filemime) {
  1214. if (
  1215. thumbNode.closest('.catalogCell') ||
  1216. /^\/\.media\/t?_[a-f0-9]{40,}$/i.test(thumbnailSrc.replace(/\\/g, ''))
  1217. ) {
  1218. return thumbnailSrc;
  1219. }
  1220. return null;
  1221. }
  1222. if (isBarePngNoThumb()) {
  1223. return thumbnailSrc;
  1224. }
  1225. if (isSmallBarePngSrc()) {
  1226. return thumbnailSrc;
  1227. }
  1228. if (isSmallImage() && hasExtension(thumbnailSrc)) {
  1229. return thumbnailSrc;
  1230. }
  1231. if (isTThumb(thumbnailSrc)) {
  1232. let base = thumbnailSrc.replace(/\/t_/, "/");
  1233. base = base.replace(/\.(jpe?g|jxl|png|apng|gif|avif|webp|webm|mp4|m4v|ogg|mp3|m4a|wav)$/i, "");
  1234. if (filemime && (filemime.toLowerCase() === "image/apng" || filemime.toLowerCase() === "video/x-m4v")) {
  1235. return base;
  1236. }
  1237.  
  1238. const ext = filemime ? getExtensionForMimeType(filemime) : null;
  1239. if (!ext) return null;
  1240. return base + ext;
  1241. }
  1242. if (isDirectHash(thumbnailSrc)) {
  1243. if (filemime && (filemime.toLowerCase() === "image/apng" || filemime.toLowerCase() === "video/x-m4v")) {
  1244. return thumbnailSrc;
  1245. }
  1246. const ext = filemime ? getExtensionForMimeType(filemime) : null;
  1247. if (!ext) {
  1248. return thumbnailSrc;
  1249. }
  1250. return thumbnailSrc + ext;
  1251. }
  1252. if (isGenericThumb()) {
  1253. if (parentA && parentA.getAttribute("href")) {
  1254. return sanitizeUrl(parentA.getAttribute("href"));
  1255. }
  1256. return null;
  1257. }
  1258.  
  1259. return null;
  1260. }
  1261. function leaveHandler() {
  1262. cleanupFloatingMedia();
  1263. }
  1264. function mouseMoveHandler(ev) {
  1265. lastMouseEvent = ev;
  1266. positionFloatingMedia(ev);
  1267. }
  1268. async function onThumbEnter(e) {
  1269. cleanupFloatingMedia();
  1270. lastMouseEvent = e;
  1271. const thumb = e.currentTarget;
  1272. let filemime = null, fullSrc = null, isVideo = false, isAudio = false;
  1273. if (thumb.tagName === "IMG") {
  1274. const parentA = thumb.closest("a.linkThumb, a.imgLink");
  1275. if (!parentA) return;
  1276. const href = parentA.getAttribute("href");
  1277. if (!href) return;
  1278. const ext = href.split(".").pop().toLowerCase();
  1279. filemime =
  1280. parentA.getAttribute("data-filemime") ||
  1281. {
  1282. jpg: "image/jpeg",
  1283. jpeg: "image/jpeg",
  1284. jxl: "image/jxl",
  1285. png: "image/png",
  1286. apng: "image/apng",
  1287. gif: "image/gif",
  1288. avif: "image/avif",
  1289. webp: "image/webp",
  1290. bmp: "image/bmp",
  1291. mp4: "video/mp4",
  1292. webm: "video/webm",
  1293. m4v: "video/x-m4v",
  1294. ogg: "audio/ogg",
  1295. mp3: "audio/mpeg",
  1296. m4a: "audio/x-m4a",
  1297. wav: "audio/wav",
  1298. }[ext];
  1299. fullSrc = getFullMediaSrc(thumb, filemime);
  1300. isVideo = filemime && filemime.startsWith("video/");
  1301. isAudio = filemime && filemime.startsWith("audio/");
  1302. }
  1303. fullSrc = sanitizeUrl(fullSrc);
  1304. if (!fullSrc) return;
  1305. let volume = 0.5;
  1306. try {
  1307. if (typeof getSetting === "function") {
  1308. const v = await getSetting("hoverVideoVolume");
  1309. if (typeof v === "number" && !isNaN(v)) {
  1310. volume = Math.max(0, Math.min(1, v / 100));
  1311. }
  1312. }
  1313. } catch { }
  1314.  
  1315. if (isAudio) {
  1316. const container = thumb.closest("a.linkThumb, a.imgLink");
  1317. if (container && !container.style.position) {
  1318. container.style.position = "relative";
  1319. }
  1320. floatingMedia = document.createElement("audio");
  1321. floatingMedia.src = fullSrc;
  1322. floatingMedia.controls = false;
  1323. floatingMedia.style.display = "none";
  1324. floatingMedia.volume = volume;
  1325. document.body.appendChild(floatingMedia);
  1326. floatingMedia.play().catch(() => { });
  1327. const indicator = document.createElement("div");
  1328. indicator.classList.add("audio-preview-indicator");
  1329. indicator.textContent = AUDIO_INDICATOR_TEXT;
  1330. if (container) {
  1331. container.appendChild(indicator);
  1332. }
  1333. currentAudioIndicator = indicator;
  1334. thumb.addEventListener("mouseleave", leaveHandler, { once: true });
  1335. if (container) container.addEventListener("click", leaveHandler, { once: true });
  1336. window.addEventListener("scroll", leaveHandler, { passive: true, once: true });
  1337. cleanupFns.push(() => thumb.removeEventListener("mouseleave", leaveHandler));
  1338. if (container) cleanupFns.push(() => container.removeEventListener("click", leaveHandler));
  1339. cleanupFns.push(() => window.removeEventListener("scroll", leaveHandler));
  1340. return;
  1341. }
  1342. floatingMedia = isVideo ? document.createElement("video") : document.createElement("img");
  1343. floatingMedia.src = fullSrc;
  1344. floatingMedia.id = "hover-preview-media";
  1345. floatingMedia.style.position = "fixed";
  1346. floatingMedia.style.zIndex = "9999";
  1347. floatingMedia.style.pointerEvents = "none";
  1348. floatingMedia.style.opacity = MEDIA_OPACITY_LOADING;
  1349. floatingMedia.style.left = "-9999px";
  1350. floatingMedia.style.top = "-9999px";
  1351. floatingMedia.style.maxWidth = MEDIA_MAX_WIDTH;
  1352. const availableHeight = window.innerHeight * (1 - MEDIA_BOTTOM_MARGIN / 100);
  1353. floatingMedia.style.maxHeight = `${availableHeight}px`;
  1354. if (isVideo) {
  1355. floatingMedia.autoplay = true;
  1356. floatingMedia.loop = true;
  1357. floatingMedia.muted = false;
  1358. floatingMedia.playsInline = true;
  1359. floatingMedia.volume = volume;
  1360. }
  1361. document.body.appendChild(floatingMedia);
  1362. document.addEventListener("mousemove", mouseMoveHandler, { passive: true });
  1363. thumb.addEventListener("mouseleave", leaveHandler, { passive: true, once: true });
  1364. cleanupFns.push(() => document.removeEventListener("mousemove", mouseMoveHandler));
  1365. if (lastMouseEvent) {
  1366. positionFloatingMedia(lastMouseEvent);
  1367. }
  1368. if (isVideo) {
  1369. floatingMedia.onloadeddata = function () {
  1370. if (floatingMedia) {
  1371. floatingMedia.style.opacity = MEDIA_OPACITY_LOADED;
  1372. if (lastMouseEvent) positionFloatingMedia(lastMouseEvent);
  1373. }
  1374. };
  1375. } else {
  1376. floatingMedia.onload = function () {
  1377. if (floatingMedia) {
  1378. floatingMedia.style.opacity = MEDIA_OPACITY_LOADED;
  1379. if (lastMouseEvent) positionFloatingMedia(lastMouseEvent);
  1380. }
  1381. };
  1382. }
  1383. floatingMedia.onerror = cleanupFloatingMedia;
  1384. thumb.addEventListener("mouseleave", leaveHandler, { once: true });
  1385. window.addEventListener("scroll", leaveHandler, { passive: true, once: true });
  1386. cleanupFns.push(() => thumb.removeEventListener("mouseleave", leaveHandler));
  1387. cleanupFns.push(() => window.removeEventListener("scroll", leaveHandler));
  1388. }
  1389. function attachThumbListeners(root = document) {
  1390. root.querySelectorAll("a.linkThumb > img, a.imgLink > img").forEach(thumb => {
  1391. if (!thumb._fullImgHoverBound) {
  1392. thumb.addEventListener("mouseenter", onThumbEnter);
  1393. thumb._fullImgHoverBound = true;
  1394. }
  1395. });
  1396. if (
  1397. root.tagName === "IMG" &&
  1398. root.parentElement &&
  1399. (root.parentElement.matches("a.linkThumb") || root.parentElement.matches("a.imgLink")) &&
  1400. !root._fullImgHoverBound
  1401. ) {
  1402. root.addEventListener("mouseenter", onThumbEnter);
  1403. root._fullImgHoverBound = true;
  1404. }
  1405. }
  1406. attachThumbListeners();
  1407. const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
  1408. if (divThreadsObs) {
  1409. divThreadsObs.addHandler(function imageHoverHandler(mutations, node) {
  1410. for (const mutation of mutations) {
  1411. for (const addedNode of mutation.addedNodes) {
  1412. if (addedNode.nodeType === 1) {
  1413. attachThumbListeners(addedNode);
  1414. }
  1415. }
  1416. }
  1417. });
  1418. }
  1419. }
  1420. function getExtensionForMimeType(mime) {
  1421. const map = {
  1422. "image/jpeg": ".jpg",
  1423. "image/jpg": ".jpg",
  1424. "image/jxl": ".jxl",
  1425. "image/png": ".png",
  1426. "image/apng": ".png",
  1427. "image/gif": ".gif",
  1428. "image/avif": ".avif",
  1429. "image/webp": ".webp",
  1430. "image/bmp": ".bmp",
  1431. };
  1432. return map[mime.toLowerCase()] || "";
  1433. }
  1434.  
  1435. async function featureBlurSpoilers() {
  1436. if (!(window.pageType?.isThread || window.pageType?.isIndex)) {
  1437. return;
  1438. }
  1439.  
  1440. const removeSpoilers = await getSetting("blurSpoilers_removeSpoilers");
  1441. function applyBlurOrRemoveSpoilers(img, removeSpoilers) {
  1442. if (removeSpoilers) {
  1443. img.style.filter = "";
  1444. img.style.transition = "";
  1445. img.style.border = "1px dotted var(--border-color)";
  1446. img.onmouseover = null;
  1447. img.onmouseout = null;
  1448. } else {
  1449. img.style.filter = "blur(5px)";
  1450. img.style.transition = "filter 0.3s ease";
  1451. }
  1452. }
  1453.  
  1454. function processImgLink(link) {
  1455. if (link.dataset.blurSpoilerProcessed === "1") {
  1456. return;
  1457. }
  1458. const img = link.querySelector("img");
  1459. if (!img) {
  1460. return;
  1461. }
  1462. if (
  1463. /\/\.media\/[^\/]+?\.[a-zA-Z0-9]+$/.test(img.src) &&
  1464. !/\/\.media\/t_[^\/]+?\.[a-zA-Z0-9]+$/.test(img.src)
  1465. ) {
  1466. link.dataset.blurSpoilerProcessed = "1";
  1467. return;
  1468. }
  1469. const isCustomSpoiler = img.src.includes("/custom.spoiler")
  1470. || img.src.includes("/*/custom.spoiler")
  1471. || img.src.includes("/spoiler.png");
  1472. const isNotThumbnail = !img.src.includes("/.media/t_");
  1473. const hasFilenameExtension = !isCustomSpoiler && /\.[a-zA-Z0-9]+$/.test(img.src);
  1474.  
  1475. if (isNotThumbnail || isCustomSpoiler) {
  1476. let href = link.getAttribute("href");
  1477. if (!href) {
  1478. link.dataset.blurSpoilerProcessed = "1";
  1479. return;
  1480. }
  1481. const match = href.match(/\/\.media\/([^\/]+)\.[a-zA-Z0-9]+$/);
  1482. if (!match) {
  1483. link.dataset.blurSpoilerProcessed = "1";
  1484. return;
  1485. }
  1486. const fileMime = link.getAttribute("data-filemime") || "";
  1487. const ext = getExtensionForMimeType(fileMime);
  1488. let fileWidthAttr = link.getAttribute("data-filewidth");
  1489. let fileHeightAttr = link.getAttribute("data-fileheight");
  1490. let transformedSrc;
  1491. if (
  1492. (fileWidthAttr && Number(fileWidthAttr) <= 220) ||
  1493. (fileHeightAttr && Number(fileHeightAttr) <= 220)
  1494. ) {
  1495. transformedSrc = `/.media/${match[1]}${ext}`;
  1496. } else if (!hasFilenameExtension && isCustomSpoiler) {
  1497. transformedSrc = `/.media/t_${match[1]}`;
  1498. } else {
  1499. link.dataset.blurSpoilerProcessed = "1";
  1500. return;
  1501. }
  1502. if (isCustomSpoiler && !fileWidthAttr && !fileHeightAttr) {
  1503. const uploadCell = img.closest('.uploadCell');
  1504. if (uploadCell) {
  1505. const dimensionLabel = uploadCell.querySelector('.dimensionLabel');
  1506. if (dimensionLabel) {
  1507. const dimensions = dimensionLabel.textContent.trim().split(/x|×/);
  1508. if (dimensions.length === 2) {
  1509. const parsedWidth = parseInt(dimensions[0].trim(), 10);
  1510. const parsedHeight = parseInt(dimensions[1].trim(), 10);
  1511. if ((parsedWidth <= 220 || parsedHeight <= 220)) {
  1512. img.src = href;
  1513. link.dataset.blurSpoilerProcessed = "1";
  1514. applyBlurOrRemoveSpoilers(img, removeSpoilers);
  1515. return;
  1516. }
  1517. }
  1518. }
  1519. }
  1520. }
  1521. const initialWidth = img.offsetWidth;
  1522. const initialHeight = img.offsetHeight;
  1523. img.style.width = initialWidth + "px";
  1524. img.style.height = initialHeight + "px";
  1525. img.src = transformedSrc;
  1526. img.addEventListener('load', function () {
  1527. img.style.width = img.naturalWidth + "px";
  1528. img.style.height = img.naturalHeight + "px";
  1529. });
  1530.  
  1531. applyBlurOrRemoveSpoilers(img, removeSpoilers);
  1532. link.dataset.blurSpoilerProcessed = "1";
  1533. return;
  1534. }
  1535. link.dataset.blurSpoilerProcessed = "1";
  1536. }
  1537. document.querySelectorAll("a.imgLink").forEach(link => processImgLink(link));
  1538. let pendingImgLinks = new WeakSet();
  1539. let debounceTimeout = null;
  1540. function processPendingImgLinks() {
  1541. const linksToProcess = Array.from(document.querySelectorAll("a.imgLink")).filter(link => pendingImgLinks.has(link));
  1542. linksToProcess.forEach(link => processImgLink(link));
  1543. pendingImgLinks = new WeakSet();
  1544. debounceTimeout = null;
  1545. }
  1546. const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
  1547. if (divThreadsObs) {
  1548. divThreadsObs.addHandler(function blurSpoilersHandler(mutations) {
  1549. for (const mutation of mutations) {
  1550. for (const addedNode of mutation.addedNodes) {
  1551. if (addedNode.nodeType !== 1) continue;
  1552. if (addedNode.classList && addedNode.classList.contains('imgLink')) {
  1553. pendingImgLinks.add(addedNode);
  1554. } else if (addedNode.querySelectorAll) {
  1555. addedNode.querySelectorAll('.imgLink').forEach(link => pendingImgLinks.add(link));
  1556. }
  1557. }
  1558. }
  1559. if (!debounceTimeout) {
  1560. debounceTimeout = setTimeout(processPendingImgLinks, 50);
  1561. }
  1562. });
  1563. }
  1564. const bodyObs = observeSelector('body', { childList: true, subtree: true });
  1565. if (bodyObs) {
  1566. bodyObs.addHandler(function quoteTooltipSpoilerHandler(mutations) {
  1567. for (const mutation of mutations) {
  1568. for (const addedNode of mutation.addedNodes) {
  1569. if (addedNode.nodeType !== 1) continue;
  1570. if (addedNode.classList && addedNode.classList.contains('quoteTooltip')) {
  1571. addedNode.querySelectorAll('a.imgLink').forEach(link => processImgLink(link));
  1572. } else if (addedNode.querySelectorAll) {
  1573. addedNode.querySelectorAll('.quoteTooltip a.imgLink').forEach(link => processImgLink(link));
  1574. }
  1575. }
  1576. }
  1577. });
  1578. }
  1579. document.body.addEventListener("mouseover", function (e) {
  1580. if (e.target.matches("a.imgLink img[style*='blur(5px)']")) {
  1581. e.target.style.filter = "none";
  1582. }
  1583. });
  1584. document.body.addEventListener("mouseout", function (e) {
  1585. if (e.target.matches("a.imgLink img[style*='transition']")) {
  1586. e.target.style.filter = "blur(5px)";
  1587. }
  1588. });
  1589. }
  1590. function autoHideHeaderOnScroll() {
  1591. const header = document.getElementById('dynamicHeaderThread');
  1592. if (!header) return;
  1593. const scrollThreshold = 50;
  1594. let lastScrollY = window.scrollY;
  1595. let scrollDirection = 'none';
  1596. let ticking = false;
  1597.  
  1598. function updateHeaderVisibility() {
  1599. const currentScrollY = window.scrollY;
  1600. scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up';
  1601. lastScrollY = currentScrollY;
  1602. const isNearTop = currentScrollY < 100;
  1603. if (scrollDirection === 'up' || isNearTop) {
  1604. header.classList.remove('nav-hidden');
  1605. } else if (scrollDirection === 'down' && currentScrollY > scrollThreshold) {
  1606. header.classList.add('nav-hidden');
  1607. }
  1608.  
  1609. ticking = false;
  1610. }
  1611. const style = document.createElement('style');
  1612. style.textContent = `
  1613. #dynamicHeaderThread {
  1614. transition: transform 0.3s ease;
  1615. }
  1616. #dynamicHeaderThread.nav-hidden {
  1617. transform: translateY(-100%);
  1618. }
  1619. :root.bottom-header #dynamicHeaderThread.nav-hidden {
  1620. transform: translateY(100%);
  1621. }
  1622. `;
  1623. document.head.appendChild(style);
  1624. window.addEventListener('scroll', () => {
  1625. if (!ticking) {
  1626. window.requestAnimationFrame(updateHeaderVisibility);
  1627. ticking = true;
  1628. }
  1629. }, { passive: true });
  1630. updateHeaderVisibility();
  1631. }
  1632. const decodeHtmlEntitiesTwice = (() => {
  1633. const txt = document.createElement('textarea');
  1634. return function (html) {
  1635. txt.innerHTML = html;
  1636. const once = txt.value;
  1637. txt.innerHTML = once;
  1638. return txt.value;
  1639. };
  1640. })();
  1641. function highlightMentions() {
  1642. const watchedCells = document.querySelectorAll("#watchedMenu .watchedCell");
  1643. const watchButton = document.querySelector(".opHead .watchButton");
  1644. if (!watchedCells.length) return;
  1645. watchedCells.forEach((cell) => {
  1646. const notification = cell.querySelector(".watchedCellLabel span.watchedNotification");
  1647. if (!notification) return;
  1648.  
  1649. const labelLink = cell.querySelector(".watchedCellLabel a");
  1650. if (!labelLink) return;
  1651. if (!labelLink.dataset.board) {
  1652. const href = labelLink.getAttribute("href");
  1653. const match = href?.match(/^(?:https?:\/\/[^\/]+)?\/([^\/]+)\//);
  1654. if (match) {
  1655. labelLink.dataset.board = `/${match[1]}/ -`;
  1656. }
  1657. if (document.location.href.includes(href)) {
  1658. if (watchButton) {
  1659. watchButton.style.color = "var(--board-title-color)";
  1660. watchButton.title = "Watched";
  1661. }
  1662. }
  1663. const originalText = labelLink.textContent;
  1664. const decodedText = decodeHtmlEntitiesTwice(originalText);
  1665. if (labelLink.textContent !== decodedText) {
  1666. labelLink.textContent = decodedText;
  1667. }
  1668. }
  1669. const notificationText = notification.textContent.trim();
  1670.  
  1671. function styleMentionYou(labelLink, notification, totalReplies) {
  1672. labelLink.style.color = "var(--board-title-color)";
  1673. notification.style.color = "var(--board-title-color)";
  1674. notification.textContent = ` (${totalReplies}) (You)`;
  1675. notification.style.fontWeight = "bold";
  1676. }
  1677.  
  1678. function styleMentionNumber(notification, notificationText) {
  1679. notification.textContent = ` (${notificationText})`;
  1680. notification.style.color = "var(--link-color)";
  1681. notification.style.fontWeight = "bold";
  1682. }
  1683. if (notificationText.startsWith("(") === true) {
  1684. return;
  1685. }
  1686. if (notificationText.includes("(you)") === true) {
  1687. const parts = notificationText.split(", ");
  1688. const totalReplies = parts[0];
  1689. styleMentionYou(labelLink, notification, totalReplies);
  1690. }
  1691. else if (/^\d+$/.test(notificationText)) {
  1692. styleMentionNumber(notification, notificationText);
  1693. }
  1694. notification.dataset.processed = "true";
  1695. });
  1696. }
  1697. const watchedMenuObs = observeSelector('#watchedMenu', { childList: true, subtree: true });
  1698. if (watchedMenuObs) {
  1699. watchedMenuObs.addHandler(function highlightMentionsHandler() {
  1700. highlightMentions();
  1701. });
  1702. }
  1703. highlightMentions();
  1704. async function featureWatchThreadOnReply() {
  1705. if ((window.pageType?.isIndex || window.pageType?.isCatalog)) {
  1706. return;
  1707. }
  1708. const getWatchButton = () => document.querySelector(".watchButton");
  1709. function watchThreadIfNotWatched() {
  1710. const btn = getWatchButton();
  1711. if (btn && !btn.classList.contains("watched-active")) {
  1712. btn.click();
  1713. setTimeout(() => {
  1714. btn.classList.add("watched-active");
  1715. }, 100);
  1716. }
  1717. }
  1718. function updateWatchButtonClass() {
  1719. const btn = getWatchButton();
  1720. if (!btn) return;
  1721. if (btn.classList.contains("watched-active")) {
  1722. btn.classList.add("watched-active");
  1723. } else {
  1724. btn.classList.remove("watched-active");
  1725. }
  1726. }
  1727. const submitButton = document.getElementById("qrbutton");
  1728. if (submitButton) {
  1729. submitButton.removeEventListener("click", submitButton._watchThreadHandler || (() => { }));
  1730. submitButton._watchThreadHandler = async function () {
  1731. if (await getSetting("watchThreadOnReply")) {
  1732. setTimeout(watchThreadIfNotWatched, 500);
  1733. }
  1734. };
  1735. submitButton.addEventListener("click", submitButton._watchThreadHandler);
  1736. }
  1737. updateWatchButtonClass();
  1738. const btn = getWatchButton();
  1739. if (btn) {
  1740. btn.removeEventListener("click", btn._updateWatchHandler || (() => { }));
  1741. btn._updateWatchHandler = () => setTimeout(updateWatchButtonClass, 100);
  1742. btn.addEventListener("click", btn._updateWatchHandler);
  1743. }
  1744. }
  1745. async function featureAlwaysShowTW() {
  1746. if (!(await getSetting("alwaysShowTW"))) return;
  1747. if ((await getSetting("alwaysShowTW_noPinInCatalog")) && window.pageType.isCatalog) return;
  1748.  
  1749. function showThreadWatcher() {
  1750. const watchedMenu = document.getElementById("watchedMenu");
  1751. if (watchedMenu) {
  1752. watchedMenu.style.display = "flex";
  1753. }
  1754. }
  1755.  
  1756. showThreadWatcher();
  1757. }
  1758. (function markAllThreadsAsRead() {
  1759. const handleDiv = document.querySelector('#watchedMenu > div.handle');
  1760. if (!handleDiv) return;
  1761. if (handleDiv.querySelector('.watchedCellDismissButton.markAllRead')) return;
  1762. const btn = document.createElement('a');
  1763. btn.className = 'watchedCellDismissButton glowOnHover coloredIcon markAllRead';
  1764. btn.title = 'Mark all threads as read';
  1765. btn.style.float = 'right';
  1766. btn.style.paddingTop = '3px';
  1767. function hasUnreadThreads() {
  1768. const watchedMenu = document.querySelector('#watchedMenu > div.floatingContainer');
  1769. if (!watchedMenu) return false;
  1770. return watchedMenu.querySelectorAll('td.watchedCellDismissButton.glowOnHover.coloredIcon[title="Mark as read"]').length > 0;
  1771. }
  1772. function updateButtonState() {
  1773. if (hasUnreadThreads()) {
  1774. btn.style.opacity = '1';
  1775. btn.style.pointerEvents = 'auto';
  1776. btn.title = 'Mark all threads as read';
  1777. } else {
  1778. btn.style.opacity = '0.5';
  1779. btn.style.pointerEvents = 'none';
  1780. btn.title = 'No unread threads';
  1781. }
  1782. }
  1783. function clickAllMarkAsReadButtons(watchedMenu) {
  1784. const markButtons = watchedMenu.querySelectorAll('td.watchedCellDismissButton.glowOnHover.coloredIcon[title="Mark as read"]');
  1785. markButtons.forEach(btn => {
  1786. try {
  1787. btn.click();
  1788. } catch (e) {
  1789. console.log("Error clicking button:", e);
  1790. }
  1791. });
  1792. return markButtons.length;
  1793. }
  1794. function markAllThreadsAsReadWithRetry(retriesLeft, callback) {
  1795. setTimeout(function () {
  1796. const watchedMenu = document.querySelector('#watchedMenu > div.floatingContainer');
  1797. if (!watchedMenu) {
  1798. if (callback) callback();
  1799. return;
  1800. }
  1801. const clickedCount = clickAllMarkAsReadButtons(watchedMenu);
  1802. if (clickedCount === 0) {
  1803. updateButtonState();
  1804. if (callback) callback();
  1805. return;
  1806. }
  1807. if (retriesLeft > 0) {
  1808. setTimeout(() => markAllThreadsAsReadWithRetry(retriesLeft - 1, callback), 200);
  1809. } else if (callback) {
  1810. callback();
  1811. }
  1812. }, 100);
  1813. }
  1814. const watchedMenuObs = observeSelector('#watchedMenu > div.floatingContainer', { childList: true, subtree: true });
  1815. if (watchedMenuObs) {
  1816. const debouncedUpdate = debounce(updateButtonState, 100);
  1817. watchedMenuObs.addHandler(function markAllThreadsAsReadHandler() {
  1818. debouncedUpdate();
  1819. });
  1820. }
  1821. updateButtonState();
  1822. handleDiv.appendChild(btn);
  1823. document.body.addEventListener('click', function (e) {
  1824. const closeBtn = e.target.closest('#watchedMenu .close-btn');
  1825. if (closeBtn) {
  1826. const watchedMenu = document.getElementById("watchedMenu");
  1827. if (watchedMenu) watchedMenu.style.display = "none";
  1828. return;
  1829. }
  1830. const markAllBtn = e.target.closest('.watchedCellDismissButton.markAllRead');
  1831. if (markAllBtn) {
  1832. e.preventDefault();
  1833. if (markAllBtn.style.pointerEvents === 'none' || markAllBtn.dataset.processing === 'true') return;
  1834. markAllBtn.dataset.processing = 'true';
  1835. markAllBtn.style.opacity = '0.5';
  1836. markAllThreadsAsReadWithRetry(3, function () {
  1837. markAllBtn.dataset.processing = 'false';
  1838. updateButtonState();
  1839. });
  1840. }
  1841. });
  1842. })();
  1843. function hashNavigation() {
  1844. if (!window.pageType?.isThread) return;
  1845. const processedLinks = new WeakSet();
  1846. function addHashLinks(container = document) {
  1847. const links = container.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
  1848. links.forEach(link => {
  1849. if (
  1850. processedLinks.has(link) ||
  1851. (link.nextSibling && link.nextSibling.classList && link.nextSibling.classList.contains('hash-link-container'))
  1852. ) return;
  1853. const hashSpan = document.createElement('span');
  1854. hashSpan.textContent = ' #';
  1855. hashSpan.className = 'hash-link';
  1856. hashSpan.style.cursor = 'pointer';
  1857. hashSpan.style.color = 'var(--navbar-text-color)';
  1858. hashSpan.title = 'Scroll to post';
  1859. const wrapper = document.createElement('span');
  1860. wrapper.className = 'hash-link-container';
  1861. wrapper.appendChild(hashSpan);
  1862.  
  1863. link.insertAdjacentElement('afterend', wrapper);
  1864. processedLinks.add(link);
  1865. });
  1866. }
  1867. addHashLinks();
  1868. const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
  1869. if (divThreadsObs) {
  1870. const debouncedAddHashLinks = debounce(() => addHashLinks(), 25);
  1871. divThreadsObs.addHandler(function hashNavigationHandler() {
  1872. debouncedAddHashLinks();
  1873. });
  1874. }
  1875. const postsContainer = document.getElementById('divThreads') || document.body;
  1876. postsContainer.addEventListener('click', function (e) {
  1877. if (e.target.classList.contains('hash-link')) {
  1878. e.preventDefault();
  1879. const link = e.target.closest('.hash-link-container').previousElementSibling;
  1880. if (!link || !link.href) return;
  1881. const hashMatch = link.href.match(/#(\d+)$/);
  1882. if (!hashMatch) return;
  1883. const postId = hashMatch[1];
  1884. const safePostId = /^[0-9]+$/.test(postId) ? postId : null;
  1885. if (!safePostId) return;
  1886. const postElem = document.getElementById(safePostId);
  1887. if (postElem) {
  1888. window.location.hash = `#${safePostId}`;
  1889. if (postElem.classList.contains('opCell')) {
  1890. const offset = 25;
  1891. const rect = postElem.getBoundingClientRect();
  1892. const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  1893. const targetY = rect.top + scrollTop - offset;
  1894. window.scrollTo({ top: targetY, behavior: "smooth" });
  1895. } else {
  1896. postElem.scrollIntoView({ behavior: "smooth", block: "center" });
  1897. }
  1898. }
  1899. }
  1900. }, true);
  1901. }
  1902. function featureScrollArrows() {
  1903. if (document.getElementById("scroll-arrow-up") || document.getElementById("scroll-arrow-down")) {
  1904. return;
  1905. }
  1906. const upBtn = document.createElement("button");
  1907. upBtn.id = "scroll-arrow-up";
  1908. upBtn.className = "scroll-arrow-btn";
  1909. upBtn.title = "Scroll to top";
  1910. upBtn.innerHTML = "▲";
  1911. upBtn.addEventListener("click", () => {
  1912. window.scrollTo({ top: 0, behavior: "smooth" });
  1913. });
  1914. const downBtn = document.createElement("button");
  1915. downBtn.id = "scroll-arrow-down";
  1916. downBtn.className = "scroll-arrow-btn";
  1917. downBtn.title = "Scroll to bottom";
  1918. downBtn.innerHTML = "▼";
  1919. downBtn.addEventListener("click", () => {
  1920. window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
  1921. });
  1922.  
  1923. document.body.appendChild(upBtn);
  1924. document.body.appendChild(downBtn);
  1925. }
  1926. async function featureHideAnnouncement() {
  1927. async function processElement(selector, settingKey, contentKey) {
  1928. const el = document.querySelector(selector);
  1929. if (!el) return;
  1930.  
  1931. const content = (el.textContent || "").replace(/[^\w\s.,!?-]/g, "");
  1932. const shouldHide = await GM.getValue(`8chanSS_${settingKey}`, "false") === "true";
  1933. const storedContent = await GM.getValue(`8chanSS_${contentKey}`, null);
  1934. const root = document.documentElement;
  1935.  
  1936. if (shouldHide) {
  1937. if (storedContent !== null && storedContent !== content) {
  1938. if (typeof window.setSetting === "function") {
  1939. await window.setSetting("hideAnnouncement", false);
  1940. }
  1941. await GM.setValue(`8chanSS_${settingKey}`, "false");
  1942. await GM.deleteValue(`8chanSS_${contentKey}`);
  1943. return;
  1944. }
  1945. root.classList.add("hide-announcement");
  1946. await GM.setValue(`8chanSS_${contentKey}`, content);
  1947. } else {
  1948. root.classList.remove("hide-announcement");
  1949. await GM.deleteValue(`8chanSS_${contentKey}`);
  1950. }
  1951. }
  1952.  
  1953. await processElement("#dynamicAnnouncement", "hideAnnouncement", "announcementContent");
  1954. }
  1955. (async function featureBeepOnYou() {
  1956. if (!divPosts) return;
  1957. let audioContext = null;
  1958. let audioContextReady = false;
  1959. let audioContextPromise = null;
  1960. function ensureAudioContextReady() {
  1961. if (audioContextReady) return Promise.resolve();
  1962. if (audioContextPromise) return audioContextPromise;
  1963.  
  1964. audioContextPromise = new Promise((resolve) => {
  1965. function resumeAudioContext() {
  1966. if (!audioContext) {
  1967. audioContext = new (window.AudioContext || window.webkitAudioContext)();
  1968. }
  1969. if (audioContext.state === 'suspended') {
  1970. audioContext.resume().then(() => {
  1971. audioContextReady = true;
  1972. window.removeEventListener('click', resumeAudioContext);
  1973. window.removeEventListener('keydown', resumeAudioContext);
  1974. resolve();
  1975. });
  1976. } else {
  1977. audioContextReady = true;
  1978. window.removeEventListener('click', resumeAudioContext);
  1979. window.removeEventListener('keydown', resumeAudioContext);
  1980. resolve();
  1981. }
  1982. }
  1983. window.addEventListener('click', resumeAudioContext);
  1984. window.addEventListener('keydown', resumeAudioContext);
  1985. });
  1986. return audioContextPromise;
  1987. }
  1988.  
  1989. async function createBeepSound() {
  1990. if (!(await getSetting("beepOnYou"))) {
  1991. return;
  1992. }
  1993. await ensureAudioContextReady();
  1994.  
  1995. return function playBeep() {
  1996. try {
  1997. const oscillator = audioContext.createOscillator();
  1998. const gainNode = audioContext.createGain();
  1999.  
  2000. oscillator.type = 'sine';
  2001. oscillator.frequency.value = 550;
  2002. gainNode.gain.value = 0.1;
  2003.  
  2004. oscillator.connect(gainNode);
  2005. gainNode.connect(audioContext.destination);
  2006. oscillator.start();
  2007. setTimeout(() => {
  2008. oscillator.stop();
  2009. }, 100);
  2010. } catch (e) {
  2011. console.warn("Beep failed:", e);
  2012. }
  2013. };
  2014. }
  2015. window.originalTitle = document.title;
  2016. window.isNotifying = false;
  2017. let beepOnYouSetting = false;
  2018. let notifyOnYouSetting = false;
  2019. let customMsgSetting = "(!) ";
  2020. let previousFaviconState = null;
  2021. async function initSettings() {
  2022. beepOnYouSetting = await getSetting("beepOnYou");
  2023. notifyOnYouSetting = await getSetting("notifyOnYou");
  2024. const customMsg = await getSetting("notifyOnYou_customMessage");
  2025. if (customMsg) customMsgSetting = customMsg;
  2026. }
  2027. await initSettings();
  2028. let playBeep = null;
  2029. createBeepSound().then(fn => { playBeep = fn; });
  2030. let scrollHandlerActive = false;
  2031. async function notifyOnYou() {
  2032. if (!window.isNotifying) {
  2033. window.isNotifying = true;
  2034. document.title = customMsgSetting + " " + window.originalTitle;
  2035. if (await getSetting("customFavicon")) {
  2036. const { style, state } = faviconManager.getCurrentFaviconState();
  2037. if (state !== "notif") {
  2038. previousFaviconState = { style, state };
  2039. }
  2040. faviconManager.setFaviconStyle(style, "notif");
  2041. }
  2042. }
  2043. }
  2044. function setupNotificationScrollHandler() {
  2045. if (scrollHandlerActive) return;
  2046. scrollHandlerActive = true;
  2047. const BOTTOM_OFFSET = 45;
  2048. function checkScrollPosition() {
  2049. if (!window.isNotifying) return;
  2050. const scrollPosition = window.scrollY + window.innerHeight;
  2051. const documentHeight = document.documentElement.scrollHeight;
  2052. if (scrollPosition >= documentHeight - BOTTOM_OFFSET) {
  2053. document.title = window.originalTitle;
  2054. window.isNotifying = false;
  2055. const { state } = faviconManager.getCurrentFaviconState();
  2056. if (state === "notif" && previousFaviconState) {
  2057. faviconManager.setFaviconStyle(previousFaviconState.style, previousFaviconState.state);
  2058. previousFaviconState = null;
  2059. } else if (state === "notif") {
  2060. faviconManager.setFavicon("base");
  2061. }
  2062. window.removeEventListener('scroll', checkScrollPosition);
  2063. scrollHandlerActive = false;
  2064. }
  2065. }
  2066. window.addEventListener('scroll', checkScrollPosition);
  2067. }
  2068. window.addEventListener("focus", () => {
  2069. if (window.isNotifying) {
  2070. setupNotificationScrollHandler();
  2071. }
  2072. });
  2073. const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: false });
  2074. if (divPostsObs) {
  2075. divPostsObs.addHandler(function beepOnYouHandler(mutations) {
  2076. for (const mutation of mutations) {
  2077. for (const node of mutation.addedNodes) {
  2078. if (
  2079. node.nodeType === 1 &&
  2080. typeof node.matches === "function" &&
  2081. (node.matches('.postCell') || node.matches('.opCell')) &&
  2082. node.querySelector("a.quoteLink.you") &&
  2083. !node.closest('.innerPost')
  2084. ) {
  2085. if (beepOnYouSetting && playBeep) {
  2086. playBeep();
  2087. }
  2088. if (notifyOnYouSetting) {
  2089. notifyOnYou();
  2090. setupNotificationScrollHandler();
  2091. }
  2092. }
  2093. }
  2094. }
  2095. });
  2096. }
  2097. window.addEventListener("8chanSS_settingChanged", async (e) => {
  2098. if (e.detail && e.detail.key) {
  2099. const key = e.detail.key;
  2100. if (key === "beepOnYou") {
  2101. beepOnYouSetting = await getSetting("beepOnYou");
  2102. createBeepSound().then(fn => { playBeep = fn; });
  2103. } else if (key === "notifyOnYou") {
  2104. notifyOnYouSetting = await getSetting("notifyOnYou");
  2105. } else if (key === "notifyOnYou_customMessage") {
  2106. const customMsg = await getSetting("notifyOnYou_customMessage");
  2107. if (customMsg) customMsgSetting = customMsg;
  2108. }
  2109. }
  2110. });
  2111. })();
  2112. async function enhanceYouTubeLinks() {
  2113. if (!(window.pageType?.isThread || window.pageType?.isIndex)) {
  2114. return;
  2115. }
  2116. const ytThumbsEnabled = await getSetting("enhanceYoutube_ytThumbs");
  2117. const ytTitleCache = {};
  2118. const MAX_CACHE_SIZE = 350;
  2119. const ORDER_KEY = "_order";
  2120. const TRACKING_PARAMS = [
  2121. "si", "feature", "ref", "fsi", "source",
  2122. "utm_source", "utm_medium", "utm_campaign", "gclid", "gclsrc", "fbclid"
  2123. ];
  2124. const ytThumbCache = {};
  2125. function loadCache() {
  2126. try {
  2127. const data = localStorage.getItem('ytTitleCache');
  2128. if (data) {
  2129. const parsed = JSON.parse(data);
  2130. Object.assign(ytTitleCache, parsed);
  2131. if (!Array.isArray(ytTitleCache[ORDER_KEY])) {
  2132. ytTitleCache[ORDER_KEY] = [];
  2133. }
  2134. } else {
  2135. ytTitleCache[ORDER_KEY] = [];
  2136. }
  2137. } catch (e) {
  2138. ytTitleCache[ORDER_KEY] = [];
  2139. }
  2140. }
  2141. function saveCache() {
  2142. try {
  2143. localStorage.setItem('ytTitleCache', JSON.stringify(ytTitleCache));
  2144. } catch (e) { }
  2145. }
  2146. loadCache();
  2147. function getYouTubeId(url) {
  2148. try {
  2149. const u = new URL(url);
  2150. if (u.hostname === 'youtu.be') {
  2151. return u.pathname.slice(1);
  2152. }
  2153. if (u.hostname.endsWith('youtube.com')) {
  2154. if (u.pathname === '/watch') {
  2155. return u.searchParams.get('v');
  2156. }
  2157. const liveMatch = u.pathname.match(/^\/(live|embed|shorts)\/([a-zA-Z0-9_-]{11})/);
  2158. if (liveMatch) {
  2159. return liveMatch[2];
  2160. }
  2161. }
  2162. } catch (e) { }
  2163. return null;
  2164. }
  2165. function sanitizeYouTubeId(videoId) {
  2166. if (!videoId) return null;
  2167. const match = videoId.match(/([a-zA-Z0-9_-]{11})/);
  2168. return match ? match[1] : null;
  2169. }
  2170. function stripTrackingParams(url) {
  2171. try {
  2172. const u = new URL(url);
  2173. let changed = false;
  2174. const KEEP_PARAMS = new Set(['t', 'start']);
  2175. TRACKING_PARAMS.forEach(param => {
  2176. if (u.searchParams.has(param) && !KEEP_PARAMS.has(param)) {
  2177. u.searchParams.delete(param);
  2178. changed = true;
  2179. }
  2180. });
  2181. if (u.hash && u.hash.includes('?')) {
  2182. const [hashPath, hashQuery] = u.hash.split('?');
  2183. const hashParams = new URLSearchParams(hashQuery);
  2184. let hashChanged = false;
  2185. TRACKING_PARAMS.forEach(param => {
  2186. if (hashParams.has(param) && !KEEP_PARAMS.has(param)) {
  2187. hashParams.delete(param);
  2188. hashChanged = true;
  2189. }
  2190. });
  2191. if (hashChanged) {
  2192. u.hash = hashParams.toString()
  2193. ? `${hashPath}?${hashParams.toString()}`
  2194. : hashPath;
  2195. changed = true;
  2196. }
  2197. }
  2198. return changed ? u.toString() : url;
  2199. } catch (e) {
  2200. return url;
  2201. }
  2202. }
  2203.  
  2204. async function fetchYouTubeTitle(videoId) {
  2205. const cleanId = sanitizeYouTubeId(videoId);
  2206. if (!cleanId) return null;
  2207. if (ytTitleCache.hasOwnProperty(cleanId)) {
  2208. const idx = ytTitleCache[ORDER_KEY].indexOf(cleanId);
  2209. if (idx !== -1) {
  2210. ytTitleCache[ORDER_KEY].splice(idx, 1);
  2211. }
  2212. ytTitleCache[ORDER_KEY].push(cleanId);
  2213. saveCache();
  2214. return ytTitleCache[cleanId];
  2215. }
  2216. try {
  2217. const r = await fetch(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${cleanId}&format=json`);
  2218. if (!r.ok) return null;
  2219. const data = await r.json();
  2220. const title = data ? data.title : null;
  2221. if (title) {
  2222. ytTitleCache[cleanId] = title;
  2223. ytTitleCache[ORDER_KEY].push(cleanId);
  2224. while (ytTitleCache[ORDER_KEY].length > MAX_CACHE_SIZE) {
  2225. const oldest = ytTitleCache[ORDER_KEY].shift();
  2226. delete ytTitleCache[oldest];
  2227. }
  2228. saveCache();
  2229. }
  2230. return title;
  2231. } catch {
  2232. return null;
  2233. }
  2234. }
  2235. async function fetchAsDataURL(url) {
  2236. return new Promise((resolve) => {
  2237. GM.xmlHttpRequest({
  2238. method: "GET",
  2239. url: url,
  2240. responseType: "blob",
  2241. timeout: 8000,
  2242. onload: (response) => {
  2243. if (response.status === 200 && response.response) {
  2244. const reader = new FileReader();
  2245. reader.onloadend = () => resolve(reader.result);
  2246. reader.onerror = () => resolve(null);
  2247. reader.readAsDataURL(response.response);
  2248. } else {
  2249. resolve(null);
  2250. }
  2251. },
  2252. onerror: () => resolve(null),
  2253. ontimeout: () => resolve(null)
  2254. });
  2255. });
  2256. }
  2257. async function fetchYouTubeThumbnailAsDataURL(videoId) {
  2258. if (ytThumbCache.hasOwnProperty(videoId)) {
  2259. return ytThumbCache[videoId];
  2260. }
  2261. const webpUrl = `https://i.ytimg.com/vi_webp/${videoId}/hqdefault.webp`;
  2262. const jpgUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
  2263.  
  2264. let dataUrl = await fetchAsDataURL(webpUrl);
  2265. if (!dataUrl) {
  2266. dataUrl = await fetchAsDataURL(jpgUrl);
  2267. }
  2268. ytThumbCache[videoId] = dataUrl;
  2269. return dataUrl;
  2270. }
  2271. function addThumbnailHover(link, videoId) {
  2272. if (link.dataset.ytThumbHover) return;
  2273. link.dataset.ytThumbHover = "1";
  2274. let thumbDiv = null;
  2275. let lastImg = null;
  2276. let lastHoverToken = 0;
  2277.  
  2278. function showThumb(e) {
  2279. if (!thumbDiv) {
  2280. thumbDiv = document.createElement('div');
  2281. thumbDiv.style.position = 'fixed';
  2282. thumbDiv.style.zIndex = 9999;
  2283. thumbDiv.style.pointerEvents = 'none';
  2284. thumbDiv.style.background = '#222';
  2285. thumbDiv.style.border = '1px solid #444';
  2286. thumbDiv.style.padding = '2px';
  2287. thumbDiv.style.borderRadius = '4px';
  2288. thumbDiv.style.boxShadow = '0 2px 8px rgba(0,0,0,0.4)';
  2289. thumbDiv.style.transition = 'opacity 0.1s';
  2290. thumbDiv.style.opacity = '0';
  2291. thumbDiv.style.maxWidth = '280px';
  2292. thumbDiv.style.maxHeight = '200px';
  2293.  
  2294. const img = document.createElement('img');
  2295. img.style.display = 'block';
  2296. img.style.maxWidth = '280px';
  2297. img.style.maxHeight = '200px';
  2298. img.style.borderRadius = '3px';
  2299. img.alt = "YouTube thumbnail";
  2300. img.src = "data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQv///wAAACH5BAEAAAMALAAAAAAQABAAAAIgjI+py+0Po5yUFQA7";
  2301.  
  2302. lastImg = img;
  2303. const hoverToken = ++lastHoverToken;
  2304.  
  2305. fetchYouTubeThumbnailAsDataURL(videoId).then(dataUrl => {
  2306. if (lastImg === img && hoverToken === lastHoverToken) {
  2307. if (dataUrl) {
  2308. img.src = dataUrl;
  2309. } else {
  2310. img.alt = "Failed to load thumbnail";
  2311. }
  2312. }
  2313. });
  2314.  
  2315. thumbDiv.appendChild(img);
  2316. document.body.appendChild(thumbDiv);
  2317.  
  2318. setTimeout(() => {
  2319. if (thumbDiv) thumbDiv.style.opacity = '1';
  2320. }, 10);
  2321. }
  2322. const top = Math.min(window.innerHeight - 130, e.clientY + 12);
  2323. const left = Math.min(window.innerWidth - 290, e.clientX + 12);
  2324. thumbDiv.style.top = `${top}px`;
  2325. thumbDiv.style.left = `${left}px`;
  2326. }
  2327.  
  2328. function moveThumb(e) {
  2329. if (thumbDiv) {
  2330. const top = Math.min(window.innerHeight - 130, e.clientY + 12);
  2331. const left = Math.min(window.innerWidth - 290, e.clientX + 12);
  2332. thumbDiv.style.top = `${top}px`;
  2333. thumbDiv.style.left = `${left}px`;
  2334. }
  2335. }
  2336.  
  2337. function hideThumb() {
  2338. lastHoverToken++;
  2339. if (thumbDiv && thumbDiv.parentNode) {
  2340. thumbDiv.parentNode.removeChild(thumbDiv);
  2341. thumbDiv = null;
  2342. }
  2343. lastImg = null;
  2344. }
  2345.  
  2346. link.addEventListener('mouseenter', showThumb);
  2347. link.addEventListener('mousemove', moveThumb);
  2348. link.addEventListener('mouseleave', hideThumb);
  2349. }
  2350.  
  2351. function processLinks(root = document) {
  2352. root.querySelectorAll('a[href*="youtu"]').forEach(link => {
  2353. if (link.dataset.ytEnhanced) return;
  2354. const videoId = getYouTubeId(link.href);
  2355. const cleanId = sanitizeYouTubeId(videoId);
  2356. if (!cleanId) return;
  2357. link.dataset.ytEnhanced = "1";
  2358. const cleanUrl = stripTrackingParams(link.href);
  2359. if (cleanUrl !== link.href) {
  2360. link.href = cleanUrl;
  2361. }
  2362. fetchYouTubeTitle(cleanId).then(title => {
  2363. if (title) {
  2364. link.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="18" height="16" style="vertical-align:middle;margin-right:2px;"><path fill="#FF0000" d="M549.7 124.1c-6.3-23.7-24.9-42.4-48.6-48.6C456.5 64 288 64 288 64s-168.5 0-213.1 11.5c-23.7 6.3-42.4 24.9-48.6 48.6C16 168.5 16 256 16 256s0 87.5 10.3 131.9c6.3 23.7 24.9 42.4 48.6 48.6C119.5 448 288 448 288 448s168.5 0 213.1-11.5c23.7-6.3 42.4-24.9 48.6-48.6 10.3-44.4 10.3-131.9 10.3-131.9s0-87.5-10.3-131.9zM232 334.1V177.9L361 256 232 334.1z"/></svg><span></span> ${title}`;
  2365. }
  2366. });
  2367. if (ytThumbsEnabled) {
  2368. addThumbnailHover(link, cleanId);
  2369. }
  2370. });
  2371. }
  2372. processLinks(document);
  2373. const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
  2374. if (divThreadsObs) {
  2375. divThreadsObs.addHandler(function enhanceYoutubeLinksHandler(mutations) {
  2376. for (const mutation of mutations) {
  2377. for (const addedNode of mutation.addedNodes) {
  2378. if (addedNode.nodeType === 1) {
  2379. processLinks(addedNode);
  2380. }
  2381. }
  2382. }
  2383. });
  2384. }
  2385. }
  2386. function featureLabelCreated12h() {
  2387. if (window.pageType?.isCatalog) {
  2388. return;
  2389. }
  2390.  
  2391. function convertLabelCreatedSpan(span) {
  2392. if (span.dataset.timeConverted === "1") return;
  2393.  
  2394. const text = span.textContent;
  2395. const match = text.match(/^(.+\))\s+(\d{2}):(\d{2}):(\d{2})$/);
  2396. if (!match) return;
  2397.  
  2398. const [_, datePart, hourStr, minStr, secStr] = match;
  2399. let hour = parseInt(hourStr, 10);
  2400. const min = minStr;
  2401. const sec = secStr;
  2402. const ampm = hour >= 12 ? 'PM' : 'AM';
  2403. let hour12 = hour % 12;
  2404. if (hour12 === 0) hour12 = 12;
  2405.  
  2406. const newText = `${datePart} ${hour12}:${min}:${sec} ${ampm}`;
  2407. span.textContent = newText;
  2408. span.dataset.timeConverted = "1";
  2409. }
  2410. function convertAllLabelCreated(root = document) {
  2411. const spans = root.querySelectorAll
  2412. ? root.querySelectorAll('.labelCreated')
  2413. : [];
  2414. spans.forEach(convertLabelCreatedSpan);
  2415. }
  2416.  
  2417. convertAllLabelCreated();
  2418. const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: true });
  2419. if (divPostsObs) {
  2420. divPostsObs.addHandler(function labelCreated12hHandler(mutations) {
  2421. for (const mutation of mutations) {
  2422. for (const addedNode of mutation.addedNodes) {
  2423. if (addedNode.nodeType !== 1) continue;
  2424. if (addedNode.classList && addedNode.classList.contains('labelCreated')) {
  2425. convertLabelCreatedSpan(addedNode);
  2426. } else if (addedNode.querySelectorAll) {
  2427. addedNode.querySelectorAll('.labelCreated').forEach(convertLabelCreatedSpan);
  2428. }
  2429. }
  2430. }
  2431. });
  2432. }
  2433. }
  2434. function truncateFilenames(filenameLength) {
  2435. if (window.pageType?.isCatalog) return;
  2436. if (!divThreads) return;
  2437. function processLinks(root = document) {
  2438. const links = root.querySelectorAll('a.originalNameLink');
  2439. links.forEach(link => {
  2440. if (link.dataset.truncated === "1") return;
  2441. const fullFilename = link.getAttribute('download');
  2442. if (!fullFilename) return;
  2443. const lastDot = fullFilename.lastIndexOf('.');
  2444. if (lastDot === -1) return;
  2445. const name = fullFilename.slice(0, lastDot);
  2446. const ext = fullFilename.slice(lastDot);
  2447. let truncated = fullFilename;
  2448. if (name.length > filenameLength) {
  2449. truncated = name.slice(0, filenameLength) + '(...)' + ext;
  2450. }
  2451. link.textContent = truncated;
  2452. link.dataset.truncated = "1";
  2453. link.dataset.fullFilename = fullFilename;
  2454. link.dataset.truncatedFilename = truncated;
  2455. link.title = fullFilename;
  2456. });
  2457. }
  2458. processLinks(document);
  2459. divThreads.addEventListener('mouseover', function (e) {
  2460. const link = e.target.closest('a.originalNameLink');
  2461. if (link && link.dataset.fullFilename) {
  2462. link.textContent = link.dataset.fullFilename;
  2463. }
  2464. });
  2465. divThreads.addEventListener('mouseout', function (e) {
  2466. const link = e.target.closest('a.originalNameLink');
  2467. if (link && link.dataset.truncatedFilename) {
  2468. link.textContent = link.dataset.truncatedFilename;
  2469. }
  2470. });
  2471. const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
  2472. if (divThreadsObs) {
  2473. const debouncedProcess = debounce(() => processLinks(divThreads), 100);
  2474. divThreadsObs.addHandler(function truncateFilenamesHandler() {
  2475. debouncedProcess();
  2476. });
  2477. }
  2478. }
  2479. function threadInfoHeader() {
  2480. const navHeader = document.querySelector('.navHeader');
  2481. const navOptionsSpan = document.getElementById('navOptionsSpan');
  2482. const postCountEl = document.getElementById('postCount');
  2483. const userCountEl = document.getElementById('userCountLabel');
  2484. const fileCountEl = document.getElementById('fileCount');
  2485.  
  2486. if (!(navHeader && navOptionsSpan && postCountEl && userCountEl && fileCountEl)) return;
  2487.  
  2488. function updateHeader() {
  2489. const postCount = postCountEl.textContent || '0';
  2490. const userCount = userCountEl.textContent || '0';
  2491. const fileCount = fileCountEl.textContent || '0';
  2492. let statsDisplay = navHeader.querySelector('.thread-stats-display');
  2493. if (!statsDisplay) {
  2494. statsDisplay = document.createElement('span');
  2495. statsDisplay.className = 'thread-stats-display';
  2496. statsDisplay.style.marginRight = '1px';
  2497. }
  2498.  
  2499. statsDisplay.innerHTML = `
  2500. [
  2501. <span class="statLabel">Posts: </span><span class="statNumb">${postCount}</span> |
  2502. <span class="statLabel">Users: </span><span class="statNumb">${userCount}</span> |
  2503. <span class="statLabel">Files: </span><span class="statNumb">${fileCount}</span>
  2504. ]
  2505. `;
  2506. if (statsDisplay.parentNode && statsDisplay.parentNode !== navOptionsSpan) {
  2507. statsDisplay.parentNode.removeChild(statsDisplay);
  2508. }
  2509. if (navOptionsSpan.firstChild !== statsDisplay) {
  2510. navOptionsSpan.insertBefore(statsDisplay, navOptionsSpan.firstChild);
  2511. }
  2512. }
  2513. updateHeader();
  2514. const statSelectors = ['#postCount', '#userCountLabel', '#fileCount'];
  2515. statSelectors.forEach(selector => {
  2516. const statObs = observeSelector(selector, { childList: true, subtree: false, characterData: true });
  2517. if (statObs) {
  2518. const debouncedUpdate = debounce(updateHeader, 100);
  2519. statObs.addHandler(function threadInfoHeaderHandler() {
  2520. debouncedUpdate();
  2521. });
  2522. }
  2523. });
  2524. }
  2525. function mediaViewerPositioning() {
  2526. localStorage.setItem("mediaViewer", "true");
  2527.  
  2528. async function updateMediaViewerClass() {
  2529. const mediaViewer = document.getElementById('media-viewer');
  2530. if (!mediaViewer) return;
  2531.  
  2532. const isEnabled = await getSetting("enableMediaViewer");
  2533. if (!isEnabled) {
  2534. mediaViewer.classList.remove('topright', 'topleft');
  2535. return;
  2536. }
  2537.  
  2538. const viewerStyle = await getSetting("enableMediaViewer_viewerStyle");
  2539. mediaViewer.classList.remove('topright', 'topleft');
  2540. if (viewerStyle === 'topright' || viewerStyle === 'topleft') {
  2541. mediaViewer.classList.add(viewerStyle);
  2542. }
  2543. }
  2544. updateMediaViewerClass();
  2545. const mediaViewerObs = observeSelector('#media-viewer', { childList: false, subtree: false });
  2546. if (mediaViewerObs) {
  2547. mediaViewerObs.addHandler(function mediaViewerPositioningHandler() {
  2548. updateMediaViewerClass();
  2549. });
  2550. }
  2551. const bodyObs = observeSelector('body', { childList: true, subtree: false });
  2552. if (bodyObs) {
  2553. bodyObs.addHandler(function bodyMediaViewerHandler(mutations) {
  2554. for (const mutation of mutations) {
  2555. for (const node of mutation.addedNodes) {
  2556. if (node.nodeType === 1 && node.id === 'media-viewer') {
  2557. updateMediaViewerClass();
  2558. }
  2559. }
  2560. }
  2561. });
  2562. }
  2563. }
  2564. async function featureHighlightNewIds() {
  2565. if (window.pageType?.isLast || window.pageType?.isCatalog) {
  2566. return;
  2567. }
  2568.  
  2569. const hlStyle = await getSetting("highlightNewIds_idHlStyle");
  2570. if (!divPosts) return;
  2571. if (!document.querySelector('.spanId')) return;
  2572. const styleClassMap = {
  2573. moetext: "moeText",
  2574. glow: "id-glow",
  2575. dotted: "id-dotted"
  2576. };
  2577. const styleClass = styleClassMap[hlStyle] || "moeText";
  2578. function highlightIds(root = divPosts) {
  2579. const idFrequency = {};
  2580. const labelSpans = root.querySelectorAll('.labelId');
  2581. labelSpans.forEach(span => {
  2582. const id = span.textContent.split(/[|\(]/)[0].trim();
  2583. idFrequency[id] = (idFrequency[id] || 0) + 1;
  2584. });
  2585. const seen = {};
  2586. labelSpans.forEach(span => {
  2587. const id = span.textContent.split(/[|\(]/)[0].trim();
  2588. span.classList.remove('moetext', 'id-glow', 'id-dotted');
  2589. if (!seen[id]) {
  2590. seen[id] = true;
  2591. span.classList.add(styleClass);
  2592. span.title = idFrequency[id] === 1
  2593. ? "This ID appears only once."
  2594. : "This was the first occurrence of this ID.";
  2595. } else {
  2596. span.title = "";
  2597. }
  2598. });
  2599. }
  2600. highlightIds();
  2601. const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: true });
  2602. if (divPostsObs) {
  2603. const debouncedHighlightIds = debounce(() => highlightIds(), 50);
  2604. divPostsObs.addHandler(function highlightNewIdsHandler(mutations) {
  2605. let needsUpdate = false;
  2606. for (const mutation of mutations) {
  2607. for (const node of mutation.addedNodes) {
  2608. if (
  2609. node.nodeType === 1 &&
  2610. (node.matches?.('.labelId') || node.querySelector?.('.labelId'))
  2611. ) {
  2612. needsUpdate = true;
  2613. break;
  2614. }
  2615. }
  2616. if (needsUpdate) break;
  2617. }
  2618. if (needsUpdate) {
  2619. debouncedHighlightIds();
  2620. }
  2621. });
  2622. }
  2623. }
  2624. async function featureShowIDCount() {
  2625. if (!window.pageType?.isThread) return;
  2626. if (!document.querySelector('.spanId')) return;
  2627.  
  2628. const alwaysShowIdCount = await getSetting("alwaysShowIdCount");
  2629.  
  2630. function updateIdCounts(root = divPosts) {
  2631. const idFrequency = {};
  2632. const labelSpans = root.querySelectorAll('.labelId');
  2633. labelSpans.forEach(span => {
  2634. const id = span.textContent.split(/[|\(]/)[0].trim();
  2635. idFrequency[id] = (idFrequency[id] || 0) + 1;
  2636. });
  2637. labelSpans.forEach(span => {
  2638. const id = span.textContent.split(/[|\(]/)[0].trim();
  2639. if (alwaysShowIdCount) {
  2640. span.textContent = `${id} | ${idFrequency[id]}`;
  2641. span.onmouseover = function (e) { e.stopImmediatePropagation(); e.preventDefault(); };
  2642. span.onmouseout = function (e) { e.stopImmediatePropagation(); e.preventDefault(); };
  2643. } else {
  2644. span.textContent = id;
  2645. span.onmouseover = null;
  2646. span.onmouseout = null;
  2647. }
  2648. });
  2649. }
  2650. updateIdCounts();
  2651. const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: true });
  2652. if (divPostsObs) {
  2653. divPostsObs.addHandler(function showIdCountHandler(mutations) {
  2654. updateIdCounts();
  2655. });
  2656. }
  2657. }
  2658. async function featureQuoteThreading() {
  2659. const isEnabled = typeof getSetting === "function"
  2660. ? await getSetting("quoteThreading")
  2661. : true;
  2662.  
  2663. if (!isEnabled) {
  2664. document.querySelector('.quoteThreadingRefresh')?.remove();
  2665. return;
  2666. }
  2667. function processPosts(posts) {
  2668. posts.forEach(post => {
  2669. const backlinks = post.querySelectorAll('.panelBacklinks .backLink.postLink');
  2670.  
  2671. backlinks.forEach(backlink => {
  2672. const targetUri = backlink.getAttribute('data-target-uri');
  2673. if (!targetUri) return;
  2674.  
  2675. const targetPostId = targetUri.split('#')[1];
  2676. const targetPost = document.getElementById(targetPostId);
  2677.  
  2678. if (targetPost) {
  2679. let repliesContainer = post.nextElementSibling;
  2680. if (!repliesContainer?.classList.contains('threadedReplies')) {
  2681. repliesContainer = document.createElement('div');
  2682. repliesContainer.className = 'threadedReplies';
  2683. post.parentNode.insertBefore(repliesContainer, post.nextSibling);
  2684. }
  2685. if (!repliesContainer.contains(targetPost)) {
  2686. repliesContainer.appendChild(targetPost);
  2687. }
  2688. }
  2689. });
  2690. });
  2691. }
  2692. function threadAllPosts() {
  2693. processPosts(document.querySelectorAll('.divPosts .postCell'));
  2694. }
  2695.  
  2696. function threadNewPosts() {
  2697. const allPosts = document.querySelectorAll('.divPosts .postCell');
  2698. processPosts(Array.from(allPosts).slice(-5));
  2699. }
  2700. const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: false });
  2701. if (divPostsObs) {
  2702. divPostsObs.addHandler(function quoteThreadingHandler(mutations) {
  2703. for (const mutation of mutations) {
  2704. if (mutation.addedNodes.length) {
  2705. setTimeout(threadNewPosts, 50);
  2706. }
  2707. }
  2708. });
  2709. }
  2710. function addRefreshButton() {
  2711. const replyButton = document.querySelector('.threadBottom .innerUtility #replyButton');
  2712. if (!replyButton || replyButton.nextElementSibling?.classList.contains('quoteThreadingBtn')) return;
  2713.  
  2714. const refreshBtn = document.createElement('a');
  2715. refreshBtn.href = "#";
  2716. refreshBtn.className = "quoteThreadingBtn";
  2717. refreshBtn.title = "Refresh quote threading";
  2718. refreshBtn.textContent = "ReThread";
  2719.  
  2720. replyButton.after(' ', refreshBtn);
  2721.  
  2722. refreshBtn.addEventListener('click', e => {
  2723. e.preventDefault();
  2724. threadAllPosts();
  2725. });
  2726. }
  2727. threadAllPosts();
  2728. addRefreshButton();
  2729. }
  2730. function featureLastFifty() {
  2731. if (!window.pageType?.isCatalog) return;
  2732. if (!catalogDiv) return;
  2733. function addLastLinkButtons(root = document) {
  2734. root.querySelectorAll('.catalogCell').forEach(cell => {
  2735. const linkThumb = cell.querySelector('.linkThumb');
  2736. const threadStats = cell.querySelector('.threadStats');
  2737. if (!linkThumb || !threadStats) return;
  2738.  
  2739. const href = linkThumb.getAttribute('href');
  2740. if (!href || !/\/res\//.test(href)) return;
  2741. const lastHref = href.replace('/res/', '/last/');
  2742. threadStats.querySelectorAll('.last-link-btn').forEach(btn => btn.remove());
  2743. const span = document.createElement('span');
  2744. span.className = 'last-link-btn';
  2745. span.style.marginLeft = '0.5em';
  2746. const a = document.createElement('a');
  2747. a.href = lastHref;
  2748. a.textContent = '[L]';
  2749. a.title = 'Go to last 50 posts of this thread';
  2750. a.style.textDecoration = 'none';
  2751. a.style.fontWeight = 'bold';
  2752. span.appendChild(a);
  2753. const labelPage = threadStats.querySelector('.labelPage');
  2754. if (labelPage && labelPage.parentNode) {
  2755. labelPage.parentNode.insertBefore(span, labelPage.nextSibling);
  2756. } else {
  2757. threadStats.appendChild(span);
  2758. }
  2759. });
  2760. }
  2761. addLastLinkButtons(document);
  2762. const catalogDivObs = observeSelector('.catalogDiv', { childList: true, subtree: false });
  2763. if (catalogDivObs) {
  2764. const debouncedUpdate = debounce(() => addLastLinkButtons(document), 50);
  2765. catalogDivObs.addHandler(function lastFiftyHandler() {
  2766. debouncedUpdate();
  2767. });
  2768. }
  2769. }
  2770. function featureToggleIdAsYours() {
  2771. if (!window.pageType?.isThread) return;
  2772. if (!document.querySelector('.spanId')) return;
  2773. function getBoardName() {
  2774. const postCell = document.querySelector('.postCell[data-boarduri], .opCell[data-boarduri]');
  2775. if (postCell) return postCell.getAttribute('data-boarduri');
  2776. const match = location.pathname.match(/^\/([^\/]+)\//);
  2777. return match ? match[1] : 'unknown';
  2778. }
  2779. const BOARD_NAME = getBoardName();
  2780. const T_YOUS_KEY = `${BOARD_NAME}-yous`;
  2781. const MENU_ENTRY_CLASS = "toggleIdAsYoursMenuEntry";
  2782. const MENU_SELECTOR = ".floatingList.extraMenu";
  2783. function getYourPostNumbers() {
  2784. try {
  2785. const val = localStorage.getItem(T_YOUS_KEY);
  2786. return val ? JSON.parse(val).map(Number) : [];
  2787. } catch {
  2788. return [];
  2789. }
  2790. }
  2791. function setYourPostNumbers(arr) {
  2792. localStorage.setItem(T_YOUS_KEY, JSON.stringify(arr.map(Number)));
  2793. }
  2794. document.body.addEventListener('click', function (e) {
  2795. if (e.target.matches('.extraMenuButton')) {
  2796. const postCell = e.target.closest('.postCell, .opCell');
  2797. if (!postCell) return;
  2798. setTimeout(() => {
  2799. let menu = e.target.parentNode.querySelector('.floatingList.extraMenu');
  2800. if (!menu) {
  2801. const menus = Array.from(document.querySelectorAll('.floatingList.extraMenu'));
  2802. menu = menus[menus.length - 1];
  2803. }
  2804. if (menu) {
  2805. menu.setAttribute('data-post-id', postCell.id);
  2806. const labelIdSpan = postCell.querySelector('.labelId');
  2807. if (labelIdSpan) {
  2808. menu.setAttribute('data-label-id', labelIdSpan.textContent.trim());
  2809. }
  2810. addMenuEntries(menu.parentNode || menu);
  2811. }
  2812. }, 0);
  2813. }
  2814. });
  2815.  
  2816. function getLabelIdFromMenu(menu) {
  2817. return menu.getAttribute('data-label-id') || null;
  2818. }
  2819. function toggleYouNameClassForId(labelId, add) {
  2820. document.querySelectorAll('.postCell, .opCell').forEach(postCell => {
  2821. const labelIdSpan = postCell.querySelector('.labelId');
  2822. const rawId = labelIdSpan ? labelIdSpan.textContent.split(/[|\(]/)[0].trim() : null;
  2823. if (rawId === labelId) {
  2824. const nameLink = postCell.querySelector(".linkName.noEmailName");
  2825. if (nameLink) {
  2826. nameLink.classList.toggle("youName", add);
  2827. }
  2828. }
  2829. });
  2830. }
  2831. function getAllPostNumbersForId(labelId) {
  2832. const postNumbers = [];
  2833. document.querySelectorAll('.divPosts .postCell').forEach(postCell => {
  2834. const labelIdSpan = postCell.querySelector('.labelId');
  2835. const rawId = labelIdSpan ? labelIdSpan.textContent.split(/[|\(]/)[0].trim() : null;
  2836. if (rawId === labelId) {
  2837. const num = Number(postCell.id);
  2838. if (!isNaN(num)) postNumbers.push(num);
  2839. }
  2840. });
  2841. return postNumbers;
  2842. }
  2843. function addMenuEntries(root = document) {
  2844. root.querySelectorAll(MENU_SELECTOR).forEach(menu => {
  2845. if (!menu.closest('.extraMenuButton')) return;
  2846. const ul = menu.querySelector("ul");
  2847. if (!ul || ul.querySelector("." + MENU_ENTRY_CLASS)) return;
  2848. let labelId = getLabelIdFromMenu(menu);
  2849. if (!labelId) return;
  2850. labelId = labelId.split(/[|\(]/)[0].trim();
  2851. const yourPostNumbers = getYourPostNumbers();
  2852. const postNumbersForId = getAllPostNumbersForId(labelId);
  2853. const isMarked = postNumbersForId.length > 0 && postNumbersForId.every(num => yourPostNumbers.includes(num));
  2854.  
  2855. const li = document.createElement("li");
  2856. li.className = MENU_ENTRY_CLASS;
  2857. li.style.cursor = "pointer";
  2858. li.textContent = "Toggle ID as Yours";
  2859.  
  2860. ul.appendChild(li);
  2861.  
  2862. li.addEventListener("click", function (e) {
  2863. e.stopPropagation();
  2864. let labelId = getLabelIdFromMenu(menu);
  2865. if (!labelId) return;
  2866. labelId = labelId.split(/[|\(]/)[0].trim();
  2867. let yourPostNumbers = getYourPostNumbers();
  2868. const postNumbersForId = getAllPostNumbersForId(labelId);
  2869.  
  2870. if (postNumbersForId.length === 0) return;
  2871.  
  2872. const allMarked = postNumbersForId.every(num => yourPostNumbers.includes(num));
  2873. if (!allMarked) {
  2874. postNumbersForId.forEach(num => {
  2875. if (!yourPostNumbers.includes(num)) yourPostNumbers.push(num);
  2876. });
  2877. setYourPostNumbers(yourPostNumbers);
  2878. toggleYouNameClassForId(labelId, true);
  2879. } else {
  2880. yourPostNumbers = yourPostNumbers.filter(num => !postNumbersForId.includes(num));
  2881. setYourPostNumbers(yourPostNumbers);
  2882. toggleYouNameClassForId(labelId, false);
  2883. }
  2884. });
  2885. toggleYouNameClassForId(labelId, isMarked);
  2886. });
  2887. }
  2888. window.addEventListener("storage", function (event) {
  2889. if (event.key === T_YOUS_KEY) {
  2890. const yourPostNumbers = getYourPostNumbers();
  2891. document.querySelectorAll('.postCell, .opCell').forEach(postCell => {
  2892. const nameLink = postCell.querySelector(".linkName.noEmailName");
  2893. if (nameLink) {
  2894. const postNum = Number(postCell.id);
  2895. nameLink.classList.toggle("youName", yourPostNumbers.includes(postNum));
  2896. }
  2897. });
  2898. }
  2899. });
  2900. const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
  2901. if (divThreadsObs) {
  2902. const debouncedObserverCallback = debounce((mutations) => {
  2903. for (const mutation of mutations) {
  2904. for (const node of mutation.addedNodes) {
  2905. if (node.nodeType !== 1) continue;
  2906. if (node.matches && node.matches(MENU_SELECTOR)) {
  2907. if (!node.hasAttribute('data-label-id')) {
  2908. const btn = node.closest('.extraMenuButton');
  2909. const postCell = btn && btn.closest('.postCell, .opCell');
  2910. if (postCell) {
  2911. const labelIdSpan = postCell.querySelector('.labelId');
  2912. if (labelIdSpan) {
  2913. node.setAttribute('data-label-id', labelIdSpan.textContent.trim());
  2914. }
  2915. }
  2916. }
  2917. addMenuEntries(node.parentNode || node);
  2918. } else if (node.querySelectorAll) {
  2919. node.querySelectorAll(MENU_SELECTOR).forEach(menu => {
  2920. if (!menu.hasAttribute('data-label-id')) {
  2921. const btn = menu.closest('.extraMenuButton');
  2922. const postCell = btn && btn.closest('.postCell, .opCell');
  2923. if (postCell) {
  2924. const labelIdSpan = postCell.querySelector('.labelId');
  2925. if (labelIdSpan) {
  2926. menu.setAttribute('data-label-id', labelIdSpan.textContent.trim());
  2927. }
  2928. }
  2929. }
  2930. addMenuEntries(menu.parentNode || menu);
  2931. });
  2932. }
  2933. }
  2934. }
  2935. }, 100);
  2936. divThreadsObs.addHandler(debouncedObserverCallback);
  2937. }
  2938. const yourPostNumbers = getYourPostNumbers();
  2939. document.querySelectorAll('.postCell, .opCell').forEach(postCell => {
  2940. const nameLink = postCell.querySelector(".linkName.noEmailName");
  2941. if (nameLink) {
  2942. const postNum = Number(postCell.id);
  2943. nameLink.classList.toggle("youName", yourPostNumbers.includes(postNum));
  2944. }
  2945. });
  2946. }
  2947. async function featureSauceLinks() {
  2948. if (!(window.pageType?.isThread || window.pageType?.isIndex)) {
  2949. return;
  2950. }
  2951. const enabled = await getSetting("enableTheSauce");
  2952. if (!enabled) return;
  2953. const services = [
  2954. {
  2955. key: "iqdb",
  2956. label: "iqdb",
  2957. enabled: await getSetting("enableTheSauce_iqdb"),
  2958. method: "post",
  2959. url: "https://iqdb.org/",
  2960. fileField: "file",
  2961. },
  2962. {
  2963. key: "saucenao",
  2964. label: "sauce",
  2965. enabled: await getSetting("enableTheSauce_saucenao"),
  2966. method: "post",
  2967. url: "https://saucenao.com/search.php",
  2968. fileField: "file",
  2969. },
  2970. {
  2971. key: "pixiv",
  2972. label: "pixiv",
  2973. enabled: await getSetting("enableTheSauce_pixiv"),
  2974. method: "pixiv",
  2975. },
  2976. ];
  2977. function getImageUrl(detailDiv) {
  2978. const parentCell = detailDiv.closest('.postCell') || detailDiv.closest('.opCell');
  2979. const imgLink = parentCell?.querySelector('.imgLink');
  2980. const img = imgLink ? imgLink.querySelector('img') : null;
  2981. if (!img) {
  2982. return null;
  2983. }
  2984.  
  2985. let imgSrc = img.getAttribute('src');
  2986. let origin = window.location.origin;
  2987. if (imgSrc.startsWith("//")) {
  2988. return window.location.protocol + imgSrc;
  2989. } else if (imgSrc.startsWith("/")) {
  2990. return origin + imgSrc;
  2991. } else if (/^https?:\/\//.test(imgSrc)) {
  2992. return imgSrc;
  2993. } else {
  2994. return origin + "/" + imgSrc;
  2995. }
  2996. }
  2997. async function fetchImageBlob(url) {
  2998. const response = await fetch(url);
  2999. if (!response.ok) throw new Error("Failed to fetch image");
  3000. return await response.blob();
  3001. }
  3002. function getPixivId(detailDiv) {
  3003. const origNameLink = detailDiv.querySelector('.originalNameLink');
  3004. if (!origNameLink) return null;
  3005. const filename = origNameLink.getAttribute('download') || origNameLink.textContent;
  3006. const match = filename && filename.match(/^(\d+)_p\d+\./);
  3007. return match ? match[1] : null;
  3008. }
  3009. function addSauceLinksToElement(detailDiv) {
  3010. if (detailDiv.classList.contains('sauceLinksProcessed')) {
  3011. return;
  3012. }
  3013. detailDiv.querySelectorAll('.sauceLinksContainer').forEach(el => el.remove());
  3014.  
  3015. const imgUrl = getImageUrl(detailDiv);
  3016. if (!imgUrl) {
  3017. return;
  3018. }
  3019.  
  3020. const container = document.createElement('div');
  3021. container.className = 'sauceLinksContainer';
  3022. container.style.marginTop = '3px';
  3023. container.style.display = 'flex';
  3024. container.style.flexWrap = 'wrap';
  3025. container.style.gap = '6px';
  3026.  
  3027. let anyLink = false;
  3028.  
  3029. services.forEach(service => {
  3030. if (!service.enabled) {
  3031. return;
  3032. }
  3033.  
  3034. const a = document.createElement('a');
  3035. a.className = 'sauceLink';
  3036. a.target = '_blank';
  3037. a.style.fontSize = '90%';
  3038. a.textContent = service.label;
  3039.  
  3040. if (service.method === "post") {
  3041. a.href = "#";
  3042. a.title = `Upload thumbnail to ${service.label}`;
  3043. a.addEventListener('click', async (e) => {
  3044. e.preventDefault();
  3045. try {
  3046. const blob = await fetchImageBlob(imgUrl);
  3047. const file = new File([blob], "image.png", { type: blob.type || "image/png" });
  3048. const form = document.createElement("form");
  3049. form.action = service.url;
  3050. form.method = "POST";
  3051. form.enctype = "multipart/form-data";
  3052. form.target = "_blank";
  3053. form.style.display = "none";
  3054.  
  3055. const input = document.createElement("input");
  3056. input.type = "file";
  3057. input.name = service.fileField;
  3058. form.appendChild(input);
  3059. const dt = new DataTransfer();
  3060. dt.items.add(file);
  3061. input.files = dt.files;
  3062.  
  3063. document.body.appendChild(form);
  3064. form.submit();
  3065. setTimeout(() => form.remove(), 10000);
  3066. } catch (err) {
  3067. alert("Failed to upload thumbnail: " + err);
  3068. }
  3069. });
  3070. } else if (service.method === "pixiv") {
  3071. const pixivId = getPixivId(detailDiv);
  3072. if (pixivId) {
  3073. a.href = `https://www.pixiv.net/artworks/${pixivId}`;
  3074. a.title = "Open Pixiv artwork";
  3075. } else {
  3076. return;
  3077. }
  3078. }
  3079.  
  3080. container.appendChild(a);
  3081. anyLink = true;
  3082. });
  3083.  
  3084. if (anyLink) {
  3085. detailDiv.classList.add('sauceLinksProcessed');
  3086. detailDiv.appendChild(container);
  3087. }
  3088. }
  3089. function observeAllUploadDetails(container = document) {
  3090. const details = container.querySelectorAll('.uploadDetails:not(.sauceLinksProcessed)');
  3091. details.forEach(detailDiv => addSauceLinksToElement(detailDiv));
  3092. }
  3093. observeAllUploadDetails();
  3094. const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: true });
  3095. if (divPostsObs) {
  3096. divPostsObs.addHandler(function sauceLinksHandler(mutations) {
  3097. for (const mutation of mutations) {
  3098. for (const node of mutation.addedNodes) {
  3099. if (node.nodeType === 1) {
  3100. if (node.classList && node.classList.contains('uploadDetails')) {
  3101. addSauceLinksToElement(node);
  3102. } else if (node.querySelectorAll) {
  3103. node.querySelectorAll('.uploadDetails:not(.sauceLinksProcessed)').forEach(addSauceLinksToElement);
  3104. }
  3105. }
  3106. }
  3107. }
  3108. });
  3109. }
  3110. const bodyObs = observeSelector('body', { childList: true, subtree: true });
  3111. if (bodyObs) {
  3112. bodyObs.addHandler(function quoteTooltipSauceLinksHandler(mutations) {
  3113. for (const mutation of mutations) {
  3114. for (const node of mutation.addedNodes) {
  3115. if (node.nodeType !== 1) continue;
  3116. if (node.classList && node.classList.contains('quoteTooltip')) {
  3117. node.querySelectorAll('.uploadDetails:not(.sauceLinksProcessed)').forEach(addSauceLinksToElement);
  3118. } else if (node.classList && node.classList.contains('innerPost')) {
  3119. node.querySelectorAll('.uploadDetails:not(.sauceLinksProcessed)').forEach(addSauceLinksToElement);
  3120. } else if (node.querySelectorAll) {
  3121. node.querySelectorAll('.quoteTooltip .uploadDetails:not(.sauceLinksProcessed)').forEach(addSauceLinksToElement);
  3122. node.querySelectorAll('.innerPost .uploadDetails:not(.sauceLinksProcessed)').forEach(addSauceLinksToElement);
  3123. }
  3124. }
  3125. }
  3126. });
  3127. }
  3128. }
  3129. function featureCustomPostHideMenu() {
  3130. const HIDDEN_POSTS_KEY = '8chanSS_hiddenPosts';
  3131. const FILTERED_NAMES_KEY = '8chanSS_filteredNames';
  3132. const FILTERED_IDS_KEY = '8chanSS_filteredIDs';
  3133. async function getStoredObject(key) {
  3134. let obj = {};
  3135. if (typeof GM !== 'undefined' && GM.getValue) {
  3136. obj = await GM.getValue(key, {});
  3137. }
  3138. return typeof obj === 'object' && obj !== null ? obj : {};
  3139. }
  3140. async function setStoredObject(key, obj) {
  3141. if (typeof GM !== 'undefined' && GM.setValue) {
  3142. await GM.setValue(key, obj);
  3143. }
  3144. }
  3145. function getAllHideButtons(root = document) {
  3146. return Array.from(root.querySelectorAll('label.hideButton'));
  3147. }
  3148. function getPostCellFromButton(btn) {
  3149. return btn.closest('.postCell, .opCell');
  3150. }
  3151. function getInnerPostElem(cell) {
  3152. return cell.querySelector('.innerPost') || cell.querySelector('.innerOP');
  3153. }
  3154. function getThreadIdFromInnerPost(inner) {
  3155. if (!inner) return null;
  3156. const dataUri = inner.getAttribute('data-uri');
  3157. if (!dataUri) return null;
  3158. const parts = dataUri.split('/');
  3159. if (parts.length < 2) return null;
  3160. return parts[1].split('#')[0];
  3161. }
  3162. function getPostId(cell) {
  3163. return cell.id ? cell.id.replace(/\D/g, '') : '';
  3164. }
  3165. function getBoardUri(cell) {
  3166. return cell.getAttribute('data-boarduri') || '';
  3167. }
  3168. function hidePostCellWithStub(cell, boardUri, postId, onUnhide, reason) {
  3169. if (!cell) return;
  3170. const inner = getInnerPostElem(cell);
  3171. if (!inner) return;
  3172. inner.classList.add('hidden');
  3173. const oldStub = cell.querySelector('.unhideButton');
  3174. if (oldStub) oldStub.remove();
  3175. const unhideBtn = document.createElement('span');
  3176. unhideBtn.className = 'unhideButton glowOnHover';
  3177. let stubText = `[Unhide post /${boardUri}/${postId}]`;
  3178. if (reason === 'filteredID') stubText += ' (filtered ID)';
  3179. else if (reason === 'filteredIDPlus') stubText += ' (reply to filtered ID)';
  3180. else if (reason === 'filteredName') stubText += ' (filtered name)';
  3181. else if (reason === 'filteredNamePlus') stubText += ' (reply to filtered name)';
  3182. else if (reason === 'hidePostPlus') stubText += ' (reply to hidden post)';
  3183. unhideBtn.textContent = stubText;
  3184. unhideBtn.addEventListener('click', function (e) {
  3185. e.stopPropagation();
  3186. inner.classList.remove('hidden');
  3187. unhideBtn.remove();
  3188. if (typeof onUnhide === 'function') onUnhide();
  3189. updateAllQuoteLinksFiltered();
  3190. });
  3191. inner.parentNode.insertBefore(unhideBtn, inner.nextSibling);
  3192. }
  3193. function unhidePostCell(cell, boardUri, postId) {
  3194. const inner = getInnerPostElem(cell);
  3195. if (inner) inner.classList.remove('hidden');
  3196. const unhideBtn = cell.querySelector('.unhideButton');
  3197. if (unhideBtn) unhideBtn.remove();
  3198. updateAllQuoteLinksFiltered();
  3199. }
  3200. function getAllRepliesRecursive(rootPostId, boardUri) {
  3201. const postMap = {};
  3202. document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
  3203. const pid = getPostId(cell);
  3204. if (pid) postMap[pid] = cell;
  3205. });
  3206. const toHide = new Set();
  3207. const visited = new Set();
  3208. const rootNum = Number(rootPostId);
  3209. const queue = [];
  3210. queue.push(rootPostId);
  3211. while (queue.length > 0) {
  3212. const currentId = queue.shift();
  3213. const postEl = postMap[currentId];
  3214. if (!postEl) continue;
  3215. const backlinks = postEl.querySelectorAll('.panelBacklinks .backLink[data-target-uri]');
  3216. backlinks.forEach(link => {
  3217. const targetUri = link.getAttribute('data-target-uri');
  3218. const match = targetUri && targetUri.match(/^([^#]+)#(\d+)$/);
  3219. if (match) {
  3220. const replyId = match[2];
  3221. const replyNum = Number(replyId);
  3222. if (!isNaN(replyNum) && replyNum > rootNum && !visited.has(replyId)) {
  3223. toHide.add(replyId);
  3224. visited.add(replyId);
  3225. queue.push(replyId);
  3226. }
  3227. }
  3228. });
  3229. }
  3230. return toHide;
  3231. }
  3232.  
  3233. async function setPostHidden(boardUri, postId, hide = true, plus = false) {
  3234. const recursiveHide = await getSetting("enableHidingMenu_recursiveHide");
  3235. document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
  3236. if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
  3237. if (getPostId(cell) === postId) {
  3238. if (hide) {
  3239. hidePostCellWithStub(cell, boardUri, postId, null, plus ? 'hidePostPlus' : undefined);
  3240. } else {
  3241. unhidePostCell(cell, boardUri, postId);
  3242. }
  3243. }
  3244. });
  3245. if (plus) {
  3246. if (recursiveHide) {
  3247. getAllRepliesRecursive(postId, boardUri).forEach(replyPid => {
  3248. document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
  3249. if (getPostId(cell) === replyPid) {
  3250. if (hide) {
  3251. hidePostCellWithStub(cell, getBoardUri(cell), getPostId(cell), null, 'hidePostPlus');
  3252. } else {
  3253. unhidePostCell(cell, getBoardUri(cell), getPostId(cell));
  3254. }
  3255. }
  3256. });
  3257. });
  3258. } else {
  3259. document.querySelectorAll('.postCell, .opCell').forEach(cell => {
  3260. if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
  3261. const quoteLinks = cell.querySelectorAll('.quoteLink[data-target-uri]');
  3262. for (const link of quoteLinks) {
  3263. const targetUri = link.getAttribute('data-target-uri');
  3264. const match = targetUri && targetUri.match(/^([^#]+)#(\w+)$/);
  3265. if (match && match[2] === postId) {
  3266. if (hide) {
  3267. hidePostCellWithStub(cell, getBoardUri(cell), getPostId(cell), null, 'hidePostPlus');
  3268. } else {
  3269. unhidePostCell(cell, getBoardUri(cell), getPostId(cell));
  3270. }
  3271. break;
  3272. }
  3273. }
  3274. });
  3275. }
  3276. }
  3277. updateAllQuoteLinksFiltered();
  3278. }
  3279. async function setPostsWithNameHidden(name, hide = true, plus = false) {
  3280. const postIdsWithName = new Set();
  3281. document.querySelectorAll('.postCell, .opCell').forEach(cell => {
  3282. if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
  3283. const nameElem = cell.querySelector('.linkName');
  3284. if (nameElem && nameElem.textContent.trim() === name) {
  3285. const boardUri = getBoardUri(cell);
  3286. const postId = getPostId(cell);
  3287. postIdsWithName.add(postId);
  3288. if (hide) {
  3289. hidePostCellWithStub(cell, boardUri, postId, null, plus ? 'filteredNamePlus' : 'filteredName');
  3290. } else {
  3291. unhidePostCell(cell, boardUri, postId);
  3292. }
  3293. }
  3294. });
  3295. if (plus && postIdsWithName.size > 0) {
  3296. document.querySelectorAll('.postCell, .opCell').forEach(cell => {
  3297. if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
  3298. const quoteLinks = cell.querySelectorAll('.quoteLink[data-target-uri]');
  3299. for (const link of quoteLinks) {
  3300. const targetUri = link.getAttribute('data-target-uri');
  3301. const match = targetUri && targetUri.match(/^([^#]+)#(\w+)$/);
  3302. if (match && postIdsWithName.has(match[2])) {
  3303. if (hide) {
  3304. hidePostCellWithStub(cell, getBoardUri(cell), getPostId(cell), null, 'filteredNamePlus');
  3305. } else {
  3306. unhidePostCell(cell, getBoardUri(cell), getPostId(cell));
  3307. }
  3308. break;
  3309. }
  3310. }
  3311. });
  3312. }
  3313. updateAllQuoteLinksFiltered();
  3314. }
  3315. async function setPostsWithIdHidden(boardUri, threadId, id, hide = true, plus = false) {
  3316. const postIdsWithId = new Set();
  3317. if (!/^[a-z0-9]+$/i.test(id)) return;
  3318. document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
  3319. if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
  3320. const inner = getInnerPostElem(cell);
  3321. const cellThreadId = getThreadIdFromInnerPost(inner);
  3322. const idElem = cell.querySelector('.labelId');
  3323. const cellId = idElem ? idElem.textContent.split(/[|\(]/)[0].trim() : null;
  3324. if (
  3325. cellThreadId === threadId &&
  3326. cellId &&
  3327. cellId === id
  3328. ) {
  3329. const postId = getPostId(cell);
  3330. postIdsWithId.add(postId);
  3331. if (hide) {
  3332. hidePostCellWithStub(cell, boardUri, postId, null, plus ? 'filteredIDPlus' : 'filteredID');
  3333. } else {
  3334. unhidePostCell(cell, boardUri, postId);
  3335. }
  3336. }
  3337. });
  3338. if (plus && postIdsWithId.size > 0) {
  3339. document.querySelectorAll('.postCell, .opCell').forEach(cell => {
  3340. if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
  3341. let isDirectReply = false;
  3342. const quoteLinks = cell.querySelectorAll('.quoteLink[data-target-uri]');
  3343. for (const link of quoteLinks) {
  3344. const targetUri = link.getAttribute('data-target-uri');
  3345. const match = targetUri && targetUri.match(/^([^#]+)#([a-z0-9]+)$/i);
  3346. if (match && postIdsWithId.has(match[2])) {
  3347. isDirectReply = true;
  3348. break;
  3349. }
  3350. }
  3351. if (!isDirectReply) {
  3352. const panelBacklinks = cell.querySelector('.panelBacklinks');
  3353. if (panelBacklinks) {
  3354. const backLinks = panelBacklinks.querySelectorAll('.backLink[data-target-uri]');
  3355. for (const link of backLinks) {
  3356. const targetUri = link.getAttribute('data-target-uri');
  3357. const match = targetUri && targetUri.match(/^([^#]+)#([a-z0-9]+)$/i);
  3358. if (match && postIdsWithId.has(match[2])) {
  3359. isDirectReply = true;
  3360. break;
  3361. }
  3362. }
  3363. }
  3364. }
  3365. if (isDirectReply) {
  3366. const replyBoardUri = getBoardUri(cell);
  3367. const replyPostId = getPostId(cell);
  3368. if (hide) {
  3369. hidePostCellWithStub(cell, replyBoardUri, replyPostId, null, 'filteredIDPlus');
  3370. } else {
  3371. unhidePostCell(cell, replyBoardUri, replyPostId);
  3372. }
  3373. }
  3374. });
  3375. }
  3376. updateAllQuoteLinksFiltered();
  3377. }
  3378. async function updateAllQuoteLinksFiltered() {
  3379. const hiddenPostsObj = await getStoredObject(HIDDEN_POSTS_KEY);
  3380. let filteredNamesObj = await getStoredObject(FILTERED_NAMES_KEY);
  3381. if (!filteredNamesObj || typeof filteredNamesObj !== "object" || Array.isArray(filteredNamesObj)) {
  3382. filteredNamesObj = { simple: Array.isArray(filteredNamesObj) ? filteredNamesObj : [], plus: [] };
  3383. }
  3384. if (!Array.isArray(filteredNamesObj.simple)) filteredNamesObj.simple = [];
  3385. if (!Array.isArray(filteredNamesObj.plus)) filteredNamesObj.plus = [];
  3386. const filteredTargets = new Set();
  3387. for (const boardUri in hiddenPostsObj) {
  3388. for (const postId of (hiddenPostsObj[boardUri]?.simple || [])) {
  3389. filteredTargets.add(`${boardUri}#${postId}`);
  3390. }
  3391. for (const postId of (hiddenPostsObj[boardUri]?.plus || [])) {
  3392. filteredTargets.add(`${boardUri}#${postId}`);
  3393. getAllRepliesRecursive(String(postId), boardUri).forEach(pid => {
  3394. filteredTargets.add(`${boardUri}#${pid}`);
  3395. });
  3396. }
  3397. }
  3398. const filteredIdsObj = await getStoredObject(FILTERED_IDS_KEY);
  3399. for (const boardUri in filteredIdsObj) {
  3400. for (const threadId in filteredIdsObj[boardUri]) {
  3401. let threadObj = filteredIdsObj[boardUri][threadId];
  3402. if (Array.isArray(threadObj)) {
  3403. threadObj = { simple: threadObj, plus: [] };
  3404. filteredIdsObj[boardUri][threadId] = threadObj;
  3405. }
  3406. for (const id of threadObj.simple || []) {
  3407. document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
  3408. const idElem = cell.querySelector('.labelId');
  3409. if (idElem && idElem.textContent.trim() === id) {
  3410. const postId = getPostId(cell);
  3411. filteredTargets.add(`${boardUri}#${postId}`);
  3412. }
  3413. });
  3414. }
  3415. for (const id of threadObj.plus || []) {
  3416. const postIdsWithId = new Set();
  3417. document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
  3418. const idElem = cell.querySelector('.labelId');
  3419. if (idElem && idElem.textContent.trim() === id) {
  3420. const postId = getPostId(cell);
  3421. filteredTargets.add(`${boardUri}#${postId}`);
  3422. postIdsWithId.add(postId);
  3423. }
  3424. });
  3425. if (postIdsWithId.size > 0) {
  3426. document.querySelectorAll('.postCell, .opCell').forEach(cell => {
  3427. const panelBacklinks = cell.querySelector('.panelBacklinks');
  3428. if (!panelBacklinks) return;
  3429. const backLinks = panelBacklinks.querySelectorAll('.backLink[data-target-uri]');
  3430. let isDirectReply = false;
  3431. for (const link of backLinks) {
  3432. const targetUri = link.getAttribute('data-target-uri');
  3433. const match = targetUri && targetUri.match(/^([^#]+)#(\d+)$/);
  3434. if (match && postIdsWithId.has(match[2])) {
  3435. isDirectReply = true;
  3436. break;
  3437. }
  3438. }
  3439. if (isDirectReply) {
  3440. const replyBoardUri = getBoardUri(cell);
  3441. const replyPostId = getPostId(cell);
  3442. filteredTargets.add(`${replyBoardUri}#${replyPostId}`);
  3443. }
  3444. });
  3445. }
  3446. }
  3447. }
  3448. }
  3449. for (const name of filteredNamesObj.simple) {
  3450. document.querySelectorAll('.postCell, .opCell').forEach(cell => {
  3451. const nameElem = cell.querySelector('.linkName');
  3452. if (nameElem && nameElem.textContent.trim() === name) {
  3453. const boardUri = getBoardUri(cell);
  3454. const postId = getPostId(cell);
  3455. filteredTargets.add(`${boardUri}#${postId}`);
  3456. }
  3457. });
  3458. }
  3459. for (const name of filteredNamesObj.plus) {
  3460. const postIdsWithName = new Set();
  3461. document.querySelectorAll('.postCell, .opCell').forEach(cell => {
  3462. const nameElem = cell.querySelector('.linkName');
  3463. if (nameElem && nameElem.textContent.trim() === name) {
  3464. const boardUri = getBoardUri(cell);
  3465. const postId = getPostId(cell);
  3466. filteredTargets.add(`${boardUri}#${postId}`);
  3467. postIdsWithName.add(postId);
  3468. }
  3469. });
  3470. postIdsWithName.forEach(pid => {
  3471. getAllRepliesRecursive(pid).forEach(replyPid => {
  3472. document.querySelectorAll('.postCell, .opCell').forEach(cell => {
  3473. if (getPostId(cell) === replyPid) {
  3474. const boardUri = getBoardUri(cell);
  3475. filteredTargets.add(`${boardUri}#${replyPid}`);
  3476. }
  3477. });
  3478. });
  3479. });
  3480. }
  3481. document.querySelectorAll('.quoteLink').forEach(link => {
  3482. let isFiltered = false;
  3483. const targetUri = link.getAttribute('data-target-uri');
  3484. if (targetUri && filteredTargets.has(targetUri)) {
  3485. isFiltered = true;
  3486. } else {
  3487. const href = link.getAttribute('href');
  3488. if (href) {
  3489. const match = href.match(/\/([^\/]+)\/res\/\d+\.html#(\d+)$/);
  3490. if (match) {
  3491. const boardUri = match[1];
  3492. const postId = match[2];
  3493. if (filteredTargets.has(`${boardUri}#${postId}`)) {
  3494. isFiltered = true;
  3495. }
  3496. }
  3497. }
  3498. }
  3499. if (isFiltered) link.classList.add('filtered');
  3500. else link.classList.remove('filtered');
  3501. });
  3502. }
  3503. async function showCustomMenu(hideButton, postCell) {
  3504. removeExistingMenu();
  3505. const extraMenu = document.createElement('div');
  3506. extraMenu.className = 'floatingList extraMenu';
  3507. extraMenu.setAttribute('data-custom', '1');
  3508. const rect = hideButton.getBoundingClientRect();
  3509. extraMenu.style.position = 'absolute';
  3510. extraMenu.style.left = `${rect.left + window.scrollX}px`;
  3511. extraMenu.style.top = `${rect.bottom + window.scrollY}px`;
  3512. extraMenu.style.zIndex = 9999;
  3513. extraMenu.style.fontSize = "10pt";
  3514. const list = document.createElement('ul');
  3515. extraMenu.appendChild(list);
  3516.  
  3517. const boardUri = getBoardUri(postCell);
  3518. const postId = getPostId(postCell);
  3519. const inner = getInnerPostElem(postCell);
  3520. const threadId = getThreadIdFromInnerPost(inner);
  3521. const idElem = postCell.querySelector('.labelId');
  3522. const id = idElem ? idElem.textContent.split(/[|\(]/)[0].trim() : null;
  3523. const nameElem = postCell.querySelector('.linkName');
  3524. const name = nameElem ? nameElem.textContent.trim() : null;
  3525. const isOP = postCell.classList.contains('opCell') || postCell.classList.contains('innerOP');
  3526. const hiddenPostsObj = await getStoredObject(HIDDEN_POSTS_KEY);
  3527. if (!hiddenPostsObj[boardUri]) hiddenPostsObj[boardUri] = { simple: [], plus: [] };
  3528. if (!Array.isArray(hiddenPostsObj[boardUri].simple)) hiddenPostsObj[boardUri].simple = [];
  3529. if (!Array.isArray(hiddenPostsObj[boardUri].plus)) hiddenPostsObj[boardUri].plus = [];
  3530. const isHiddenSimple = hiddenPostsObj[boardUri].simple.includes(Number(postId));
  3531. const isHiddenPlus = hiddenPostsObj[boardUri].plus.includes(Number(postId));
  3532. const filteredIdsObj = await getStoredObject(FILTERED_IDS_KEY);
  3533. let threadObj = filteredIdsObj[boardUri] && filteredIdsObj[boardUri][threadId];
  3534. if (Array.isArray(threadObj)) {
  3535. threadObj = { simple: threadObj, plus: [] };
  3536. filteredIdsObj[boardUri][threadId] = threadObj;
  3537. } else if (!threadObj) {
  3538. threadObj = { simple: [], plus: [] };
  3539. if (!filteredIdsObj[boardUri]) filteredIdsObj[boardUri] = {};
  3540. filteredIdsObj[boardUri][threadId] = threadObj;
  3541. }
  3542. if (!Array.isArray(threadObj.simple)) threadObj.simple = [];
  3543. if (!Array.isArray(threadObj.plus)) threadObj.plus = [];
  3544. const isFilteredId = id && threadObj.simple.includes(id);
  3545. const isFilteredIdPlus = id && threadObj.plus.includes(id);
  3546. let filteredNamesObj = await getStoredObject(FILTERED_NAMES_KEY);
  3547. if (!filteredNamesObj || typeof filteredNamesObj !== "object" || Array.isArray(filteredNamesObj)) {
  3548. filteredNamesObj = { simple: Array.isArray(filteredNamesObj) ? filteredNamesObj : [], plus: [] };
  3549. }
  3550. if (!Array.isArray(filteredNamesObj.simple)) filteredNamesObj.simple = [];
  3551. if (!Array.isArray(filteredNamesObj.plus)) filteredNamesObj.plus = [];
  3552. const isNameFiltered = name && filteredNamesObj.simple.includes(name);
  3553. const isNameFilteredPlus = name && filteredNamesObj.plus.includes(name);
  3554. const options = [];
  3555.  
  3556. if (!isOP) {
  3557. options.push(
  3558. {
  3559. name: isHiddenSimple ? 'Unhide post' : 'Hide post',
  3560. callback: async () => {
  3561. let obj = await getStoredObject(HIDDEN_POSTS_KEY);
  3562. if (!obj[boardUri]) obj[boardUri] = { simple: [], plus: [] };
  3563. let arr = obj[boardUri].simple;
  3564. const idx = arr.indexOf(Number(postId));
  3565. if (idx !== -1) {
  3566. arr.splice(idx, 1);
  3567. obj[boardUri].simple = arr;
  3568. await setStoredObject(HIDDEN_POSTS_KEY, obj);
  3569. setPostHidden(boardUri, postId, false, false);
  3570. } else {
  3571. arr.push(Number(postId));
  3572. obj[boardUri].simple = arr;
  3573. await setStoredObject(HIDDEN_POSTS_KEY, obj);
  3574. setPostHidden(boardUri, postId, true, false);
  3575. }
  3576. removeExistingMenu();
  3577. }
  3578. },
  3579. {
  3580. name: isHiddenPlus ? 'Unhide post+' : 'Hide post+',
  3581. callback: async () => {
  3582. let obj = await getStoredObject(HIDDEN_POSTS_KEY);
  3583. if (!obj[boardUri]) obj[boardUri] = { simple: [], plus: [] };
  3584. let arr = obj[boardUri].plus;
  3585. const idx = arr.indexOf(Number(postId));
  3586. if (idx !== -1) {
  3587. arr.splice(idx, 1);
  3588. obj[boardUri].plus = arr;
  3589. await setStoredObject(HIDDEN_POSTS_KEY, obj);
  3590. setPostHidden(boardUri, postId, false, true);
  3591. } else {
  3592. arr.push(Number(postId));
  3593. obj[boardUri].plus = arr;
  3594. await setStoredObject(HIDDEN_POSTS_KEY, obj);
  3595. setPostHidden(boardUri, postId, true, true);
  3596. }
  3597. removeExistingMenu();
  3598. }
  3599. }
  3600. );
  3601. }
  3602.  
  3603. options.push(
  3604. {
  3605. name: isNameFiltered ? 'Unfilter name' : 'Filter name',
  3606. callback: async () => {
  3607. let obj = await getStoredObject(FILTERED_NAMES_KEY);
  3608. if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
  3609. obj = { simple: Array.isArray(obj) ? obj : [], plus: [] };
  3610. }
  3611. if (!Array.isArray(obj.simple)) obj.simple = [];
  3612. const idx = obj.simple.indexOf(name);
  3613. if (idx !== -1) {
  3614. obj.simple.splice(idx, 1);
  3615. await setStoredObject(FILTERED_NAMES_KEY, obj);
  3616. setPostsWithNameHidden(name, false, false);
  3617. } else {
  3618. obj.simple.push(name);
  3619. await setStoredObject(FILTERED_NAMES_KEY, obj);
  3620. setPostsWithNameHidden(name, true, false);
  3621. }
  3622. removeExistingMenu();
  3623. }
  3624. },
  3625. {
  3626. name: isNameFilteredPlus ? 'Unfilter name+' : 'Filter name+',
  3627. callback: async () => {
  3628. let obj = await getStoredObject(FILTERED_NAMES_KEY);
  3629. if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
  3630. obj = { simple: Array.isArray(obj) ? obj : [], plus: [] };
  3631. }
  3632. if (!Array.isArray(obj.plus)) obj.plus = [];
  3633. const idx = obj.plus.indexOf(name);
  3634. if (idx !== -1) {
  3635. obj.plus.splice(idx, 1);
  3636. await setStoredObject(FILTERED_NAMES_KEY, obj);
  3637. setPostsWithNameHidden(name, false, true);
  3638. } else {
  3639. obj.plus.push(name);
  3640. await setStoredObject(FILTERED_NAMES_KEY, obj);
  3641. setPostsWithNameHidden(name, true, true);
  3642. }
  3643. removeExistingMenu();
  3644. }
  3645. },
  3646. {
  3647. name: isFilteredId ? 'Unfilter ID' : 'Filter ID',
  3648. callback: async () => {
  3649. let obj = await getStoredObject(FILTERED_IDS_KEY);
  3650. if (!obj[boardUri]) obj[boardUri] = {};
  3651. let threadObj = obj[boardUri][threadId];
  3652. if (Array.isArray(threadObj)) {
  3653. threadObj = { simple: threadObj, plus: [] };
  3654. obj[boardUri][threadId] = threadObj;
  3655. } else if (!threadObj) {
  3656. threadObj = { simple: [], plus: [] };
  3657. obj[boardUri][threadId] = threadObj;
  3658. }
  3659. if (!Array.isArray(threadObj.simple)) threadObj.simple = [];
  3660. let arr = threadObj.simple;
  3661. const rawId = id ? id.split(/[|\(]/)[0].trim() : id;
  3662. const idx = arr.indexOf(rawId);
  3663. if (idx !== -1) {
  3664. arr.splice(idx, 1);
  3665. threadObj.simple = arr;
  3666. await setStoredObject(FILTERED_IDS_KEY, obj);
  3667. setPostsWithIdHidden(boardUri, threadId, rawId, false, false);
  3668. } else {
  3669. arr.push(rawId);
  3670. threadObj.simple = arr;
  3671. await setStoredObject(FILTERED_IDS_KEY, obj);
  3672. setPostsWithIdHidden(boardUri, threadId, rawId, true, false);
  3673. }
  3674. removeExistingMenu();
  3675. }
  3676. },
  3677. {
  3678. name: isFilteredIdPlus ? 'Unfilter ID+' : 'Filter ID+',
  3679. callback: async () => {
  3680. let obj = await getStoredObject(FILTERED_IDS_KEY);
  3681. if (!obj[boardUri]) obj[boardUri] = {};
  3682. let threadObj = obj[boardUri][threadId];
  3683. if (Array.isArray(threadObj)) {
  3684. threadObj = { simple: threadObj, plus: [] };
  3685. obj[boardUri][threadId] = threadObj;
  3686. } else if (!threadObj) {
  3687. threadObj = { simple: [], plus: [] };
  3688. obj[boardUri][threadId] = threadObj;
  3689. }
  3690. if (!Array.isArray(threadObj.plus)) threadObj.plus = [];
  3691. let arr = threadObj.plus;
  3692. const rawId = id ? id.split(/[|\(]/)[0].trim() : id;
  3693. const idx = arr.indexOf(rawId);
  3694. if (idx !== -1) {
  3695. arr.splice(idx, 1);
  3696. threadObj.plus = arr;
  3697. await setStoredObject(FILTERED_IDS_KEY, obj);
  3698. setPostsWithIdHidden(boardUri, threadId, rawId, false, true);
  3699. } else {
  3700. arr.push(rawId);
  3701. threadObj.plus = arr;
  3702. await setStoredObject(FILTERED_IDS_KEY, obj);
  3703. setPostsWithIdHidden(boardUri, threadId, rawId, true, true);
  3704. }
  3705. removeExistingMenu();
  3706. }
  3707. }
  3708. );
  3709.  
  3710. options.forEach(opt => {
  3711. const li = document.createElement('li');
  3712. li.textContent = opt.name;
  3713. li.onclick = opt.callback;
  3714. list.appendChild(li);
  3715. });
  3716.  
  3717. document.body.appendChild(extraMenu);
  3718.  
  3719. function handleOutsideClick(e) {
  3720. if (!extraMenu.contains(e.target)) {
  3721. removeExistingMenu();
  3722. document.removeEventListener('mousedown', handleOutsideClick);
  3723. }
  3724. }
  3725. setTimeout(() => {
  3726. document.addEventListener('mousedown', handleOutsideClick);
  3727. }, 0);
  3728. }
  3729.  
  3730. function removeExistingMenu() {
  3731. document.querySelectorAll('.floatingList.extraMenu[data-custom]').forEach(menu => menu.remove());
  3732. }
  3733. function hijackHideButtons(root = document) {
  3734. getAllHideButtons(root).forEach(hideButton => {
  3735. if (hideButton.dataset.customMenuHijacked) return;
  3736. hideButton.dataset.customMenuHijacked = "1";
  3737. hideButton.onclick = null;
  3738. hideButton.addEventListener('click', function (e) {
  3739. e.preventDefault();
  3740. e.stopPropagation();
  3741. showCustomMenu(hideButton, getPostCellFromButton(hideButton));
  3742. }, true);
  3743. });
  3744. }
  3745. async function autoHideAll() {
  3746. const obj = await getStoredObject(HIDDEN_POSTS_KEY);
  3747. for (const boardUri in obj) {
  3748. const arrSimple = obj[boardUri]?.simple || [];
  3749. const arrPlus = obj[boardUri]?.plus || [];
  3750. arrSimple.forEach(postId => setPostHidden(boardUri, String(postId), true, false));
  3751. arrPlus.forEach(postId => setPostHidden(boardUri, String(postId), true, true));
  3752. }
  3753. const idsObj = await getStoredObject(FILTERED_IDS_KEY);
  3754. for (const boardUri in idsObj) {
  3755. for (const threadId in idsObj[boardUri]) {
  3756. let threadObj = idsObj[boardUri][threadId];
  3757. if (Array.isArray(threadObj)) {
  3758. threadObj = { simple: threadObj, plus: [] };
  3759. idsObj[boardUri][threadId] = threadObj;
  3760. }
  3761. (threadObj.simple || []).forEach(id => setPostsWithIdHidden(boardUri, threadId, id, true, false));
  3762. (threadObj.plus || []).forEach(id => setPostsWithIdHidden(boardUri, threadId, id, true, true));
  3763. }
  3764. }
  3765. let namesObj = await getStoredObject(FILTERED_NAMES_KEY);
  3766. if (!namesObj || typeof namesObj !== "object" || Array.isArray(namesObj)) {
  3767. namesObj = { simple: Array.isArray(namesObj) ? namesObj : [], plus: [] };
  3768. }
  3769. (namesObj.simple || []).forEach(name => setPostsWithNameHidden(name, true, false));
  3770. (namesObj.plus || []).forEach(name => setPostsWithNameHidden(name, true, true));
  3771. updateAllQuoteLinksFiltered();
  3772. }
  3773. const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: false });
  3774. if (divPostsObs) {
  3775. divPostsObs.addHandler(async function customPostHideMenuHandler(mutations) {
  3776. const hiddenPostsObj = await getStoredObject(HIDDEN_POSTS_KEY);
  3777. const filteredIdsObj = await getStoredObject(FILTERED_IDS_KEY);
  3778. let filteredNamesObj = await getStoredObject(FILTERED_NAMES_KEY);
  3779. if (!filteredNamesObj || typeof filteredNamesObj !== "object" || Array.isArray(filteredNamesObj)) {
  3780. filteredNamesObj = { simple: Array.isArray(filteredNamesObj) ? filteredNamesObj : [], plus: [] };
  3781. }
  3782. if (!Array.isArray(filteredNamesObj.simple)) filteredNamesObj.simple = [];
  3783. if (!Array.isArray(filteredNamesObj.plus)) filteredNamesObj.plus = [];
  3784. const plusHiddenMap = {};
  3785. for (const boardUri in hiddenPostsObj) {
  3786. plusHiddenMap[boardUri] = new Set((hiddenPostsObj[boardUri]?.plus || []).map(String));
  3787. }
  3788. const plusFilteredIdPostIds = {};
  3789. for (const boardUri in filteredIdsObj) {
  3790. for (const threadId in filteredIdsObj[boardUri]) {
  3791. let threadObj = filteredIdsObj[boardUri][threadId];
  3792. if (Array.isArray(threadObj)) {
  3793. threadObj = { simple: threadObj, plus: [] };
  3794. filteredIdsObj[boardUri][threadId] = threadObj;
  3795. }
  3796. if (!plusFilteredIdPostIds[boardUri]) plusFilteredIdPostIds[boardUri] = {};
  3797. plusFilteredIdPostIds[boardUri][threadId] = new Set();
  3798. for (const id of threadObj.plus || []) {
  3799. document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
  3800. const inner = getInnerPostElem(cell);
  3801. const cellThreadId = getThreadIdFromInnerPost(inner);
  3802. const idElem = cell.querySelector('.labelId');
  3803. const cellId = idElem ? idElem.textContent.split(/[|\(]/)[0].trim() : null;
  3804. if (
  3805. cellThreadId === threadId &&
  3806. cellId &&
  3807. cellId === id
  3808. ) {
  3809. plusFilteredIdPostIds[boardUri][threadId].add(getPostId(cell));
  3810. }
  3811. });
  3812. }
  3813. }
  3814. }
  3815.  
  3816. for (const mutation of mutations) {
  3817. for (const node of mutation.addedNodes) {
  3818. if (node.nodeType !== 1) continue;
  3819. let postCells = [];
  3820. if (node.classList && (node.classList.contains('postCell') || node.classList.contains('opCell'))) {
  3821. postCells.push(node);
  3822. }
  3823. if (node.querySelectorAll) {
  3824. postCells = postCells.concat(Array.from(node.querySelectorAll('.postCell, .opCell')));
  3825. }
  3826. for (const cell of postCells) {
  3827. hijackHideButtons(cell);
  3828. const boardUri = getBoardUri(cell);
  3829. const postId = getPostId(cell);
  3830. const inner = getInnerPostElem(cell);
  3831. const threadId = getThreadIdFromInnerPost(inner);
  3832. const idElem = cell.querySelector('.labelId');
  3833. const id = idElem ? idElem.textContent.split(/[|\(]/)[0].trim() : null;
  3834. const nameElem = cell.querySelector('.linkName');
  3835. const name = nameElem ? nameElem.textContent.trim() : null;
  3836. if (hiddenPostsObj[boardUri]?.simple?.includes(Number(postId))) {
  3837. setPostHidden(boardUri, postId, true, false);
  3838. }
  3839. if (hiddenPostsObj[boardUri]?.plus?.includes(Number(postId))) {
  3840. setPostHidden(boardUri, postId, true, true);
  3841. }
  3842. let threadObj = filteredIdsObj[boardUri] && filteredIdsObj[boardUri][threadId];
  3843. if (Array.isArray(threadObj)) {
  3844. threadObj = { simple: threadObj, plus: [] };
  3845. filteredIdsObj[boardUri][threadId] = threadObj;
  3846. } else if (!threadObj) {
  3847. threadObj = { simple: [], plus: [] };
  3848. if (!filteredIdsObj[boardUri]) filteredIdsObj[boardUri] = {};
  3849. filteredIdsObj[boardUri][threadId] = threadObj;
  3850. }
  3851. if (id && threadObj.simple.includes(id)) {
  3852. setPostsWithIdHidden(boardUri, threadId, id, true, false);
  3853. }
  3854. if (id && threadObj.plus.includes(id)) {
  3855. setPostsWithIdHidden(boardUri, threadId, id, true, true);
  3856. }
  3857. if (name && filteredNamesObj.simple.includes(name)) {
  3858. setPostsWithNameHidden(name, true, false);
  3859. }
  3860. if (name && filteredNamesObj.plus.includes(name)) {
  3861. setPostsWithNameHidden(name, true, true);
  3862. }
  3863. let shouldHidePlus = false;
  3864. const quoteLinks = cell.querySelectorAll('.quoteLink[data-target-uri]');
  3865. if (!window._8chanSS_postParentMapCache) {
  3866. const postParentMap = {};
  3867. document.querySelectorAll('.postCell, .opCell').forEach(postCell => {
  3868. const pid = postCell.id;
  3869. const parentIds = [];
  3870. postCell.querySelectorAll('.quoteLink[data-target-uri]').forEach(link => {
  3871. const targetUri = link.getAttribute('data-target-uri');
  3872. const match = targetUri && targetUri.match(/^([^#]+)#(\d+)$/);
  3873. if (match) parentIds.push(match[2]);
  3874. });
  3875. postParentMap[pid] = parentIds;
  3876. });
  3877. window._8chanSS_postParentMapCache = postParentMap;
  3878. }
  3879. const postParentMap = window._8chanSS_postParentMapCache;
  3880. if (!window._8chanSS_plusHiddenSetCache) {
  3881. const plusHiddenSet = new Set();
  3882. for (const b in plusHiddenMap) {
  3883. for (const hid of plusHiddenMap[b]) {
  3884. plusHiddenSet.add(hid);
  3885. }
  3886. }
  3887. window._8chanSS_plusHiddenSetCache = plusHiddenSet;
  3888. }
  3889. const plusHiddenSet = window._8chanSS_plusHiddenSetCache;
  3890. if (!window._8chanSS_filteredNamePlusSetCache) {
  3891. const filteredNamePlusSet = new Set();
  3892. const initialFiltered = [];
  3893. document.querySelectorAll('.postCell, .opCell').forEach(postCell => {
  3894. const nameElem = postCell.querySelector('.linkName');
  3895. const name = nameElem ? nameElem.textContent.trim() : null;
  3896. if (name && filteredNamesObj.plus.includes(name)) {
  3897. filteredNamePlusSet.add(postCell.id);
  3898. initialFiltered.push(postCell.id);
  3899. }
  3900. });
  3901. const postParentMap = window._8chanSS_postParentMapCache;
  3902. const childMap = {};
  3903. Object.entries(postParentMap).forEach(([childId, parentIds]) => {
  3904. parentIds.forEach(parentId => {
  3905. if (!childMap[parentId]) childMap[parentId] = [];
  3906. childMap[parentId].push(childId);
  3907. });
  3908. });
  3909. const queue = [...initialFiltered];
  3910. while (queue.length > 0) {
  3911. const current = queue.shift();
  3912. const children = childMap[current] || [];
  3913. for (const child of children) {
  3914. if (!filteredNamePlusSet.has(child)) {
  3915. filteredNamePlusSet.add(child);
  3916. queue.push(child);
  3917. }
  3918. }
  3919. }
  3920. window._8chanSS_filteredNamePlusSetCache = filteredNamePlusSet;
  3921. }
  3922. const filteredNamePlusSet = window._8chanSS_filteredNamePlusSetCache;
  3923. const visited = new Set();
  3924. function isDescendantOfPlusHiddenOrFilteredNamePlus(pid) {
  3925. if (visited.has(pid)) return false;
  3926. visited.add(pid);
  3927. if (plusHiddenSet.has(pid)) return true;
  3928. if (filteredNamePlusSet.has(pid)) return true;
  3929. const parents = postParentMap[pid] || [];
  3930. for (const par of parents) {
  3931. if (isDescendantOfPlusHiddenOrFilteredNamePlus(par)) return true;
  3932. }
  3933. return false;
  3934. }
  3935. if (isDescendantOfPlusHiddenOrFilteredNamePlus(postId)) {
  3936. shouldHidePlus = true;
  3937. }
  3938. if (shouldHidePlus) {
  3939. setPostHidden(boardUri, postId, true, true);
  3940. }
  3941. if (
  3942. plusFilteredIdPostIds[boardUri] &&
  3943. plusFilteredIdPostIds[boardUri][threadId] &&
  3944. plusFilteredIdPostIds[boardUri][threadId].size > 0
  3945. ) {
  3946. const panelBacklinks = cell.querySelector('.panelBacklinks');
  3947. let isDirectReplyToFilteredId = false;
  3948. if (panelBacklinks) {
  3949. const backLinks = panelBacklinks.querySelectorAll('.backLink[data-target-uri]');
  3950. for (const link of backLinks) {
  3951. const targetUri = link.getAttribute('data-target-uri');
  3952. const match = targetUri && targetUri.match(/^([^#]+)#(\d+)$/);
  3953. if (match && plusFilteredIdPostIds[boardUri][threadId].has(match[2])) {
  3954. isDirectReplyToFilteredId = true;
  3955. break;
  3956. }
  3957. }
  3958. }
  3959. if (!isDirectReplyToFilteredId) {
  3960. for (const link of quoteLinks) {
  3961. const targetUri = link.getAttribute('data-target-uri');
  3962. const match = targetUri && targetUri.match(/^([^#]+)#(\d+)$/);
  3963. if (match && plusFilteredIdPostIds[boardUri][threadId].has(match[2])) {
  3964. isDirectReplyToFilteredId = true;
  3965. break;
  3966. }
  3967. }
  3968. }
  3969. if (isDirectReplyToFilteredId) {
  3970. hidePostCellWithStub(cell, boardUri, postId, null, 'filteredIDPlus');
  3971. }
  3972. }
  3973. }
  3974. }
  3975. }
  3976. updateAllQuoteLinksFiltered();
  3977. });
  3978. }
  3979. hijackHideButtons();
  3980. autoHideAll();
  3981. }
  3982. async function enableIdFiltering() {
  3983. if (!window.pageType?.isThread) return;
  3984.  
  3985. const postCellSelector = ".postCell, .opCell, .innerOP";
  3986. const labelIdSelector = ".labelId";
  3987. const hiddenClassName = "is-hidden-by-filter";
  3988. let activeFilterColor = null;
  3989. const showIdLinksOnly = await getSetting("enableIdFilters_showIdLinksOnly");
  3990. const showIdLinksVertical = await getSetting("enableIdFilters_showIdLinksVertical");
  3991. let floatingDiv = null;
  3992.  
  3993. function closeFloatingDiv() {
  3994. if (floatingDiv && floatingDiv.parentNode) {
  3995. floatingDiv.parentNode.removeChild(floatingDiv);
  3996. floatingDiv = null;
  3997. }
  3998. document.removeEventListener("mousedown", outsideClickHandler, true);
  3999. }
  4000. function outsideClickHandler(e) {
  4001. if (floatingDiv && !floatingDiv.contains(e.target)) {
  4002. closeFloatingDiv();
  4003. }
  4004. }
  4005. function showIdList(id, clickedLabel) {
  4006. const idToMatch = (id.match(/^[a-fA-F0-9]{6}/) || [id.trim()])[0];
  4007.  
  4008. const threadsContainer = document.getElementById('divThreads');
  4009. if (!threadsContainer) {
  4010. return [];
  4011. }
  4012.  
  4013. const allPosts = Array.from(threadsContainer.querySelectorAll('.postCell, .opCell, .innerOP'));
  4014.  
  4015. const matchingPosts = [];
  4016. allPosts.forEach(postEl => {
  4017. const label = postEl.querySelector('.labelId');
  4018. const postId = postEl.id;
  4019. if (label && postId) {
  4020. const labelId = (label.textContent.match(/^[a-fA-F0-9]{6}/) || [label.textContent.trim()])[0];
  4021. if (labelId === idToMatch) {
  4022. matchingPosts.push(postEl);
  4023. }
  4024. }
  4025. });
  4026. document.querySelectorAll('.ss-idlinks-floating').forEach(el => el.remove());
  4027. const match = window.location.pathname.match(/^\/([^/]+)\/(res|last)\/(\d+)\.html/);
  4028. const board = match ? match[1] : '';
  4029. const thread = match ? match[3] : '';
  4030. const floatingDiv = document.createElement('div');
  4031. floatingDiv.className = 'ss-idlinks-floating';
  4032. const title = document.createElement('div');
  4033. title.style.fontWeight = 'bold';
  4034. title.style.marginBottom = '8px';
  4035. floatingDiv.appendChild(title);
  4036. const linkContainer = document.createElement('div');
  4037. if (!showIdLinksVertical) {
  4038. title.textContent = `Posts by ID: ${idToMatch} (${matchingPosts.length})`;
  4039. }
  4040. linkContainer.style.display = showIdLinksVertical ? 'block' : 'flex';
  4041. linkContainer.style.flexWrap = 'wrap';
  4042. linkContainer.style.gap = '0.3em';
  4043.  
  4044. matchingPosts.forEach(postEl => {
  4045. const postId = postEl.id;
  4046. const link = document.createElement('a');
  4047. link.className = 'quoteLink postLink';
  4048. link.href = `/${board}/res/${thread}.html#${postId}`;
  4049. link.textContent = `>>${postId}`;
  4050. link.setAttribute('data-target-uri', `${board}/${thread}#${postId}`);
  4051. link.style.display = showIdLinksVertical ? 'block' : 'inline-block';
  4052. link.onclick = function (e) {
  4053. e.preventDefault();
  4054. floatingDiv.remove();
  4055. const target = document.getElementById(postId);
  4056. if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' });
  4057. };
  4058. const wrapper = document.createElement('div');
  4059. wrapper.className = 'innerPost';
  4060. wrapper.dataset.uri = `${board}/${thread}#${postId}`;
  4061. wrapper.appendChild(link);
  4062.  
  4063. if(showIdLinksVertical){
  4064. wrapper.style.boxShadow = 'none';
  4065. wrapper.style.border = 'none';
  4066. wrapper.style.outline = 'none';
  4067. wrapper.style.backgroundColor = 'inherit';
  4068. wrapper.style.display = 'block';
  4069. wrapper.style.padding = 0;
  4070. wrapper.style.margin = 0;
  4071. }
  4072.  
  4073. linkContainer.appendChild(wrapper);
  4074. });
  4075. floatingDiv.appendChild(linkContainer);
  4076. document.body.appendChild(floatingDiv);
  4077. const rect = clickedLabel.getBoundingClientRect();
  4078. let top = rect.bottom + window.scrollY + 4;
  4079. let left = rect.left + window.scrollX;
  4080. if (left + 320 > window.innerWidth) left = Math.max(0, window.innerWidth - 340);
  4081. if (top + 200 > window.innerHeight + window.scrollY) top = Math.max(10, rect.top + window.scrollY - 220);
  4082. floatingDiv.style.top = `${top}px`;
  4083. floatingDiv.style.left = `${left}px`;
  4084. setTimeout(() => {
  4085. function closeOnClick(e) {
  4086. if (!floatingDiv.contains(e.target)) {
  4087. floatingDiv.remove();
  4088. document.removeEventListener('mousedown', closeOnClick, true);
  4089. }
  4090. }
  4091. document.addEventListener('mousedown', closeOnClick, true);
  4092. }, 0);
  4093.  
  4094. return matchingPosts;
  4095. }
  4096. function applyFilter(targetRgbColor) {
  4097. activeFilterColor = targetRgbColor;
  4098. document.querySelectorAll(postCellSelector).forEach(cell => {
  4099. const label = cell.querySelector(labelIdSelector);
  4100. const matches = label && window.getComputedStyle(label).backgroundColor === targetRgbColor;
  4101. cell.classList.toggle(hiddenClassName, !!targetRgbColor && !matches);
  4102. });
  4103. }
  4104. function handleClick(event) {
  4105. const clickedLabel = event.target.closest(labelIdSelector);
  4106. if (clickedLabel && clickedLabel.closest(postCellSelector) && !clickedLabel.closest(".de-pview")) {
  4107. event.preventDefault();
  4108. event.stopPropagation();
  4109. const id = clickedLabel.textContent.trim();
  4110. if (showIdLinksOnly) {
  4111. showIdList(id, clickedLabel);
  4112. } else {
  4113. const clickedColor = window.getComputedStyle(clickedLabel).backgroundColor;
  4114. const rect = clickedLabel.getBoundingClientRect();
  4115. const cursorOffsetY = event.clientY - rect.top;
  4116. if (activeFilterColor === clickedColor) {
  4117. applyFilter(null);
  4118. } else {
  4119. applyFilter(clickedColor);
  4120. }
  4121. clickedLabel.scrollIntoView({ behavior: "instant", block: "center" });
  4122. window.scrollBy(0, cursorOffsetY - rect.height / 2);
  4123. }
  4124. }
  4125. }
  4126. document.body.addEventListener("click", handleClick);
  4127. }
  4128. async function createSettingsMenu() {
  4129. let menu = document.getElementById("8chanSS-menu");
  4130. if (menu) return menu;
  4131. menu = document.createElement("div");
  4132. menu.id = "8chanSS-menu";
  4133. menu.style.position = "fixed";
  4134. menu.style.top = "3rem";
  4135. menu.style.left = "20rem";
  4136. menu.style.zIndex = "99999";
  4137. menu.style.background = "var(--menu-color)";
  4138. menu.style.color = "var(--text-color)";
  4139. menu.style.borderColor = "1px solid var(--border-color)";
  4140. menu.style.padding = "0";
  4141. menu.style.boxShadow = "0 4px 16px rgba(0,0,0,0.25)";
  4142. menu.style.display = "none";
  4143. menu.style.minWidth = "220px";
  4144. menu.style.width = "100%";
  4145. menu.style.maxWidth = "470px";
  4146. menu.style.fontFamily = "sans-serif";
  4147. menu.style.userSelect = "none";
  4148. let isDragging = false,
  4149. dragOffsetX = 0,
  4150. dragOffsetY = 0;
  4151. const header = document.createElement("div");
  4152. header.style.display = "flex";
  4153. header.style.justifyContent = "space-between";
  4154. header.style.alignItems = "center";
  4155. header.style.marginBottom = "0";
  4156. header.style.cursor = "move";
  4157. header.style.color = "var(--subject-color)";
  4158. header.style.background = "var(--contrast-color)";
  4159. header.style.padding = "1px 18px 1px";
  4160. header.addEventListener("mousedown", function (e) {
  4161. isDragging = true;
  4162. const rect = menu.getBoundingClientRect();
  4163. dragOffsetX = e.clientX - rect.left;
  4164. dragOffsetY = e.clientY - rect.top;
  4165. document.body.style.userSelect = "none";
  4166. });
  4167. document.addEventListener("mousemove", function (e) {
  4168. if (!isDragging) return;
  4169. let newLeft = e.clientX - dragOffsetX;
  4170. let newTop = e.clientY - dragOffsetY;
  4171. const menuRect = menu.getBoundingClientRect();
  4172. const menuWidth = menuRect.width;
  4173. const menuHeight = menuRect.height;
  4174. const viewportWidth = window.innerWidth;
  4175. const viewportHeight = window.innerHeight;
  4176. newLeft = Math.max(0, Math.min(newLeft, viewportWidth - menuWidth));
  4177. newTop = Math.max(0, Math.min(newTop, viewportHeight - menuHeight));
  4178. menu.style.left = newLeft + "px";
  4179. menu.style.top = newTop + "px";
  4180. menu.style.right = "auto";
  4181. });
  4182. document.addEventListener("mouseup", function () {
  4183. isDragging = false;
  4184. document.body.style.userSelect = "";
  4185. });
  4186. const title = document.createElement("span");
  4187. title.textContent = "8chanSS Settings";
  4188. title.style.fontWeight = "bold";
  4189. header.appendChild(title);
  4190.  
  4191. const closeBtn = document.createElement("button");
  4192. closeBtn.textContent = "✕";
  4193. closeBtn.style.background = "none";
  4194. closeBtn.style.setProperty("background", "none", "important");
  4195. closeBtn.style.border = "none";
  4196. closeBtn.style.color = "var(--subject-color)";
  4197. closeBtn.style.fontSize = "18px";
  4198. closeBtn.style.cursor = "pointer";
  4199. closeBtn.style.marginLeft = "10px";
  4200. closeBtn.addEventListener("click", () => {
  4201. menu.style.display = "none";
  4202. });
  4203. header.appendChild(closeBtn);
  4204.  
  4205. menu.appendChild(header);
  4206. const closeOnOutsideClick = (e) => {
  4207. if (menu.style.display !== "none" && !menu.contains(e.target)) {
  4208. const menuToggle = document.getElementById("8chanSS-icon");
  4209. if (menuToggle && !menuToggle.contains(e.target)) {
  4210. menu.style.display = "none";
  4211. }
  4212. }
  4213. };
  4214. Object.defineProperty(menu.style, 'display', {
  4215. set: function (value) {
  4216. const oldValue = this.getPropertyValue('display');
  4217. this.setProperty('display', value);
  4218. if (oldValue === 'none' && value !== 'none') {
  4219. setTimeout(() => {
  4220. document.addEventListener('click', closeOnOutsideClick);
  4221. }, 10);
  4222. }
  4223. else if (oldValue !== 'none' && value === 'none') {
  4224. document.removeEventListener('click', closeOnOutsideClick);
  4225. }
  4226. },
  4227. get: function () {
  4228. return this.getPropertyValue('display');
  4229. }
  4230. });
  4231. const tabNav = document.createElement("div");
  4232. tabNav.style.display = "flex";
  4233. tabNav.style.borderBottom = "1px solid #444";
  4234. tabNav.style.background = "rgb(from var(--menu-color) r g b / 1)";
  4235. const tabContent = document.createElement("div");
  4236. tabContent.style.padding = "15px 16px";
  4237. tabContent.style.maxHeight = "70vh";
  4238. tabContent.style.overflowY = "auto";
  4239. tabContent.style.scrollbarWidth = "thin";
  4240. tabContent.style.fontSize = "smaller";
  4241. const tempSettings = {};
  4242. await Promise.all(
  4243. Object.keys(flatSettings).map(async (key) => {
  4244. tempSettings[key] = await getSetting(key);
  4245. })
  4246. );
  4247. const tabs = {
  4248. site: {
  4249. label: "Site",
  4250. content: createTabContent("site", tempSettings),
  4251. },
  4252. threads: {
  4253. label: "Threads",
  4254. content: createTabContent("threads", tempSettings),
  4255. },
  4256. catalog: {
  4257. label: "Catalog",
  4258. content: createTabContent("catalog", tempSettings),
  4259. },
  4260. styling: {
  4261. label: "Style",
  4262. content: createTabContent("styling", tempSettings),
  4263. },
  4264. miscel: {
  4265. label: "Misc.",
  4266. content: createTabContent("miscel", tempSettings),
  4267. },
  4268. shortcuts: {
  4269. label: "⌨️",
  4270. content: createShortcutsTab(),
  4271. },
  4272. };
  4273. Object.keys(tabs).forEach((tabId, index, arr) => {
  4274. const tab = tabs[tabId];
  4275. const tabButton = document.createElement("button");
  4276. tabButton.textContent = tab.label;
  4277. tabButton.dataset.tab = tabId;
  4278. tabButton.style.background = index === 0 ? "var(--contrast-color)" : "transparent";
  4279. tabButton.style.border = "none";
  4280. tabButton.style.borderRight = "1px solid #444";
  4281. tabButton.style.setProperty("border-left-radius", "0", "important");
  4282. tabButton.style.color = "var(--text-color)";
  4283. tabButton.style.padding = "8px 15px";
  4284. tabButton.style.margin = "5px 0 0 0";
  4285. tabButton.style.setProperty("border-top-right-radius", "0", "important");
  4286. tabButton.style.setProperty("border-bottom-right-radius", "0", "important");
  4287. tabButton.style.cursor = "pointer";
  4288. tabButton.style.flex = "1";
  4289. tabButton.style.fontSize = "14px";
  4290. tabButton.style.transition = "background 0.2s";
  4291. if (index === 0) {
  4292. tabButton.style.setProperty("border-top-left-radius", "8px", "important");
  4293. tabButton.style.setProperty("border-top-right-radius", "0", "important");
  4294. tabButton.style.setProperty("border-bottom-left-radius", "0", "important");
  4295. tabButton.style.setProperty("border-bottom-right-radius", "0", "important");
  4296. tabButton.style.margin = "5px 0 0 5px";
  4297. }
  4298. if (index === arr.length - 1) {
  4299. tabButton.style.setProperty("border-top-right-radius", "8px", "important");
  4300. tabButton.style.setProperty("border-top-left-radius", "0", "important");
  4301. tabButton.style.setProperty("border-bottom-left-radius", "0", "important");
  4302. tabButton.style.setProperty("border-bottom-right-radius", "0", "important");
  4303. tabButton.style.margin = "5px 5px 0 0";
  4304. tabButton.style.borderRight = "none";
  4305. }
  4306.  
  4307. tabButton.addEventListener("click", () => {
  4308. Object.values(tabs).forEach((t) => {
  4309. t.content.style.display = "none";
  4310. });
  4311. tab.content.style.display = "block";
  4312. tabNav.querySelectorAll("button").forEach((btn) => {
  4313. btn.style.background = "transparent";
  4314. });
  4315. tabButton.style.background = "var(--contrast-color)";
  4316. });
  4317.  
  4318. tabNav.appendChild(tabButton);
  4319. });
  4320.  
  4321. menu.appendChild(tabNav);
  4322. Object.values(tabs).forEach((tab, index) => {
  4323. tab.content.style.display = index === 0 ? "block" : "none";
  4324. tabContent.appendChild(tab.content);
  4325. });
  4326.  
  4327. menu.appendChild(tabContent);
  4328. const buttonContainer = document.createElement("div");
  4329. buttonContainer.style.display = "flex";
  4330. buttonContainer.style.gap = "10px";
  4331. buttonContainer.style.padding = "0 18px 15px";
  4332. const saveBtn = document.createElement("button");
  4333. saveBtn.textContent = "Save";
  4334. saveBtn.style.setProperty("background", "#4caf50", "important");
  4335. saveBtn.style.setProperty("color", "#fff", "important");
  4336. saveBtn.style.border = "none";
  4337. saveBtn.style.borderRadius = "4px";
  4338. saveBtn.style.padding = "8px 18px";
  4339. saveBtn.style.fontSize = "15px";
  4340. saveBtn.style.cursor = "pointer";
  4341. saveBtn.style.flex = "1";
  4342. saveBtn.addEventListener("click", async function () {
  4343. for (const key of Object.keys(tempSettings)) {
  4344. await setSetting(key, tempSettings[key]);
  4345. }
  4346. saveBtn.textContent = "Saved!";
  4347. setTimeout(() => {
  4348. saveBtn.textContent = "Save";
  4349. }, 900);
  4350. setTimeout(() => {
  4351. window.location.reload();
  4352. }, 400);
  4353. });
  4354. buttonContainer.appendChild(saveBtn);
  4355. const resetBtn = document.createElement("button");
  4356. resetBtn.textContent = "Reset";
  4357. resetBtn.style.setProperty("background", "#dd3333", "important");
  4358. resetBtn.style.setProperty("color", "#fff", "important");
  4359. resetBtn.style.border = "none";
  4360. resetBtn.style.borderRadius = "4px";
  4361. resetBtn.style.padding = "8px 18px";
  4362. resetBtn.style.fontSize = "15px";
  4363. resetBtn.style.cursor = "pointer";
  4364. resetBtn.style.flex = "1";
  4365. resetBtn.addEventListener("click", async function () {
  4366. if (confirm("Reset all 8chanSS settings to defaults?")) {
  4367. const keys = await GM.listValues();
  4368. for (const key of keys) {
  4369. if (key.startsWith("8chanSS_")) {
  4370. await GM.deleteValue(key);
  4371. }
  4372. }
  4373. resetBtn.textContent = "Reset!";
  4374. setTimeout(() => {
  4375. resetBtn.textContent = "Reset";
  4376. }, 900);
  4377. setTimeout(() => {
  4378. window.location.reload();
  4379. }, 400);
  4380. }
  4381. });
  4382. buttonContainer.appendChild(resetBtn);
  4383.  
  4384. menu.appendChild(buttonContainer);
  4385. const info = document.createElement("div");
  4386. info.style.fontSize = "11px";
  4387. info.style.padding = "0 18px 12px";
  4388. info.style.opacity = "0.7";
  4389. info.style.textAlign = "center";
  4390. info.innerHTML = 'Press Save to apply changes. Page will reload. - <a href="https://github.com/otacoo/8chanSS/blob/main/CHANGELOG.md" target="_blank" title="Check the changelog." style="color: var(--link-color); text-decoration: underline dashed;">Ver. ' + VERSION + '</a>';
  4391. menu.appendChild(info);
  4392.  
  4393. document.body.appendChild(menu);
  4394. return menu;
  4395. }
  4396. function createTabContent(category, tempSettings) {
  4397. const container = document.createElement("div");
  4398. const categorySettings = scriptSettings[category];
  4399.  
  4400. Object.keys(categorySettings).forEach((key) => {
  4401. const setting = categorySettings[key];
  4402. if (setting.type === "separator") {
  4403. const hr = document.createElement("hr");
  4404. hr.style.border = "none";
  4405. hr.style.borderTop = "1px solid #444";
  4406. hr.style.margin = "12px 0";
  4407. container.appendChild(hr);
  4408. return;
  4409. }
  4410. if (setting.type === "title") {
  4411. const title = document.createElement("div");
  4412. title.textContent = setting.label;
  4413. title.style.fontWeight = "bold";
  4414. title.style.color = "var(--subject-title)";
  4415. title.style.fontSize = "1rem";
  4416. title.style.margin = "10px 0 6px 0";
  4417. title.style.opacity = "0.9";
  4418. container.appendChild(title);
  4419. return;
  4420. }
  4421. const parentRow = document.createElement("div");
  4422. parentRow.style.display = "flex";
  4423. parentRow.style.alignItems = "center";
  4424. parentRow.style.marginBottom = "0px";
  4425. if (key === "hoverVideoVolume" && setting.type === "number") {
  4426. const label = document.createElement("label");
  4427. label.htmlFor = "setting_" + key;
  4428. label.textContent = setting.label + ": ";
  4429. label.style.flex = "1";
  4430.  
  4431. const sliderContainer = document.createElement("div");
  4432. sliderContainer.style.display = "flex";
  4433. sliderContainer.style.alignItems = "center";
  4434. sliderContainer.style.flex = "1";
  4435.  
  4436. const slider = document.createElement("input");
  4437. slider.type = "range";
  4438. slider.id = "setting_" + key;
  4439. slider.min = setting.min;
  4440. slider.max = setting.max;
  4441. slider.value = Number(tempSettings[key]).toString();
  4442. slider.style.flex = "unset";
  4443. slider.style.width = "100px";
  4444. slider.style.marginRight = "10px";
  4445.  
  4446. const valueLabel = document.createElement("span");
  4447. valueLabel.textContent = slider.value + "%";
  4448. valueLabel.style.minWidth = "40px";
  4449. valueLabel.style.textAlign = "right";
  4450.  
  4451. slider.addEventListener("input", function () {
  4452. let val = Number(slider.value);
  4453. if (isNaN(val)) val = setting.default;
  4454. val = Math.max(setting.min, Math.min(setting.max, val));
  4455. slider.value = val.toString();
  4456. tempSettings[key] = val;
  4457. valueLabel.textContent = val + "%";
  4458. });
  4459.  
  4460. sliderContainer.appendChild(slider);
  4461. sliderContainer.appendChild(valueLabel);
  4462.  
  4463. parentRow.appendChild(label);
  4464. parentRow.appendChild(sliderContainer);
  4465. const wrapper = document.createElement("div");
  4466. wrapper.style.marginBottom = "10px";
  4467. wrapper.appendChild(parentRow);
  4468. container.appendChild(wrapper);
  4469. return;
  4470. }
  4471. const checkbox = document.createElement("input");
  4472. checkbox.type = "checkbox";
  4473. checkbox.id = "setting_" + key;
  4474. checkbox.checked =
  4475. tempSettings[key] === true || tempSettings[key] === "true";
  4476. checkbox.style.marginRight = "8px";
  4477. const label = document.createElement("label");
  4478. label.htmlFor = checkbox.id;
  4479. label.textContent = setting.label;
  4480. label.style.flex = "1";
  4481. let chevron = null;
  4482. let subOptionsContainer = null;
  4483. if (setting?.subOptions) {
  4484. chevron = document.createElement("span");
  4485. chevron.className = "ss-chevron";
  4486. chevron.innerHTML = "&#9654;";
  4487. chevron.style.display = "inline-block";
  4488. chevron.style.transition = "transform 0.2s";
  4489. chevron.style.marginLeft = "6px";
  4490. chevron.style.fontSize = "12px";
  4491. chevron.style.userSelect = "none";
  4492. chevron.style.transform = checkbox.checked
  4493. ? "rotate(90deg)"
  4494. : "rotate(0deg)";
  4495. }
  4496. checkbox.addEventListener("change", function () {
  4497. tempSettings[key] = checkbox.checked;
  4498. if (!setting?.subOptions) return;
  4499. if (!subOptionsContainer) return;
  4500.  
  4501. subOptionsContainer.style.display = checkbox.checked
  4502. ? "block"
  4503. : "none";
  4504.  
  4505. if (!chevron) return;
  4506. chevron.style.transform = checkbox.checked
  4507. ? "rotate(90deg)"
  4508. : "rotate(0deg)";
  4509. });
  4510.  
  4511. parentRow.appendChild(checkbox);
  4512. parentRow.appendChild(label);
  4513. if (chevron) parentRow.appendChild(chevron);
  4514. const wrapper = document.createElement("div");
  4515. wrapper.style.marginBottom = "10px";
  4516.  
  4517. wrapper.appendChild(parentRow);
  4518. if (setting?.subOptions) {
  4519. subOptionsContainer = document.createElement("div");
  4520. subOptionsContainer.style.marginLeft = "25px";
  4521. subOptionsContainer.style.marginTop = "5px";
  4522. subOptionsContainer.style.display = checkbox.checked ? "block" : "none";
  4523.  
  4524. Object.keys(setting.subOptions).forEach((subKey) => {
  4525. const subSetting = setting.subOptions[subKey];
  4526. const fullKey = `${key}_${subKey}`;
  4527.  
  4528. const subWrapper = document.createElement("div");
  4529. subWrapper.style.marginBottom = "5px";
  4530.  
  4531. if (subSetting.type === "text") {
  4532. const subLabel = document.createElement("label");
  4533. subLabel.htmlFor = "setting_" + fullKey;
  4534. subLabel.textContent = subSetting.label + ": ";
  4535.  
  4536. const subInput = document.createElement("input");
  4537. subInput.type = "text";
  4538. subInput.id = "setting_" + fullKey;
  4539. subInput.value = tempSettings[fullKey] || "";
  4540. subInput.maxLength = subSetting.maxLength;
  4541. subInput.style.width = "60px";
  4542. subInput.style.marginLeft = "2px";
  4543. subInput.placeholder = "(!) ";
  4544. subInput.addEventListener("input", function () {
  4545. let val = subInput.value.replace(/[<>"']/g, "");
  4546. if (val.length > subInput.maxLength) {
  4547. val = val.slice(0, subInput.maxLength);
  4548. }
  4549. subInput.value = val;
  4550. tempSettings[fullKey] = val;
  4551. });
  4552.  
  4553. subWrapper.appendChild(subLabel);
  4554. subWrapper.appendChild(subInput);
  4555. } else if (subSetting.type === "textarea") {
  4556. const subLabel = document.createElement("label");
  4557. subLabel.htmlFor = "setting_" + fullKey;
  4558. subLabel.textContent = subSetting.label + ": ";
  4559.  
  4560. const subTextarea = document.createElement("textarea");
  4561. subTextarea.id = "setting_" + fullKey;
  4562. subTextarea.value = tempSettings[fullKey] || "";
  4563. subTextarea.rows = subSetting.rows || 4;
  4564. subTextarea.style.width = "90%";
  4565. subTextarea.style.margin = "5px 0 0";
  4566. subTextarea.placeholder = subSetting.placeholder || "";
  4567.  
  4568. subTextarea.addEventListener("input", function () {
  4569. tempSettings[fullKey] = subTextarea.value;
  4570. });
  4571.  
  4572. subWrapper.appendChild(subLabel);
  4573. subWrapper.appendChild(document.createElement("br"));
  4574. subWrapper.appendChild(subTextarea);
  4575. } else if (subSetting.type === "number") {
  4576. const subLabel = document.createElement("label");
  4577. subLabel.htmlFor = "setting_" + fullKey;
  4578. subLabel.textContent = subSetting.label + ": ";
  4579.  
  4580. const subInput = document.createElement("input");
  4581. subInput.type = "number";
  4582. subInput.id = "setting_" + fullKey;
  4583. subInput.value = tempSettings[fullKey] || subSetting.default;
  4584. if (subSetting.min !== undefined) subInput.min = subSetting.min;
  4585. if (subSetting.max !== undefined) subInput.max = subSetting.max;
  4586. subInput.style.width = "60px";
  4587. subInput.style.marginLeft = "2px";
  4588.  
  4589. subInput.addEventListener("input", function () {
  4590. let val = Number(subInput.value);
  4591. if (isNaN(val)) val = subSetting.default;
  4592. if (subSetting.min !== undefined) val = Math.max(subSetting.min, val);
  4593. if (subSetting.max !== undefined) val = Math.min(subSetting.max, val);
  4594. subInput.value = val;
  4595. tempSettings[fullKey] = val;
  4596. });
  4597.  
  4598. subWrapper.appendChild(subLabel);
  4599. subWrapper.appendChild(subInput);
  4600. } else if (subSetting.type === "select") {
  4601. const subSelect = document.createElement("select");
  4602. subSelect.id = "setting_" + fullKey;
  4603. subSelect.style.marginLeft = "5px";
  4604. subSelect.style.width = "120px";
  4605. if (Array.isArray(subSetting.options)) {
  4606. subSetting.options.forEach(option => {
  4607. const optionEl = document.createElement("option");
  4608. optionEl.value = option.value;
  4609. optionEl.textContent = option.label;
  4610. if (tempSettings[fullKey] === option.value) {
  4611. optionEl.selected = true;
  4612. }
  4613. subSelect.appendChild(optionEl);
  4614. });
  4615. }
  4616. if (!subSelect.value && subSetting.default) {
  4617. subSelect.value = subSetting.default;
  4618. tempSettings[fullKey] = subSetting.default;
  4619. }
  4620. subSelect.addEventListener("change", function () {
  4621. tempSettings[fullKey] = subSelect.value;
  4622. if (key === "customFavicon" && tempSettings["customFavicon"]) {
  4623. faviconManager.setFaviconStyle(subSelect.value, "base");
  4624. }
  4625. if (key === "faviconStyle" && tempSettings["customFavicon"]) {
  4626. faviconManager.setFaviconStyle(subSelect.value, "base");
  4627. }
  4628. });
  4629.  
  4630. const subLabel = document.createElement("label");
  4631. subLabel.htmlFor = "setting_" + fullKey;
  4632. subLabel.textContent = subSetting.label || fullKey;
  4633. subLabel.style.marginLeft = "10px";
  4634.  
  4635. subWrapper.appendChild(subLabel);
  4636. subWrapper.appendChild(subSelect);
  4637. } else {
  4638. const subCheckbox = document.createElement("input");
  4639. subCheckbox.type = "checkbox";
  4640. subCheckbox.id = "setting_" + fullKey;
  4641. subCheckbox.checked = tempSettings[fullKey];
  4642. subCheckbox.style.marginRight = "8px";
  4643.  
  4644. subCheckbox.addEventListener("change", function () {
  4645. tempSettings[fullKey] = subCheckbox.checked;
  4646. });
  4647.  
  4648. const subLabel = document.createElement("label");
  4649. subLabel.htmlFor = subCheckbox.id;
  4650. subLabel.textContent = subSetting.label;
  4651.  
  4652. subWrapper.appendChild(subCheckbox);
  4653. subWrapper.appendChild(subLabel);
  4654. }
  4655. subOptionsContainer.appendChild(subWrapper);
  4656. });
  4657.  
  4658. wrapper.appendChild(subOptionsContainer);
  4659. }
  4660.  
  4661. container.appendChild(wrapper);
  4662. });
  4663.  
  4664. return container;
  4665. }
  4666. const themeSelector = document.getElementById("themesBefore");
  4667. let link = null;
  4668. let bracketSpan = null;
  4669. if (themeSelector) {
  4670. bracketSpan = document.createElement("span");
  4671. bracketSpan.textContent = "] [ ";
  4672. link = document.createElement("a");
  4673. link.id = "8chanSS-icon";
  4674. link.href = "#";
  4675. link.textContent = "8chanSS";
  4676. link.style.fontWeight = "bold";
  4677.  
  4678. themeSelector.parentNode.insertBefore(
  4679. bracketSpan,
  4680. themeSelector.nextSibling
  4681. );
  4682. themeSelector.parentNode.insertBefore(link, bracketSpan.nextSibling);
  4683. }
  4684. function createShortcutsTab() {
  4685. const container = document.createElement("div");
  4686. const title = document.createElement("h3");
  4687. title.textContent = "Keyboard Shortcuts";
  4688. title.style.margin = "0 0 15px 0";
  4689. title.style.fontSize = "16px";
  4690. container.appendChild(title);
  4691. const table = document.createElement("table");
  4692. table.style.width = "100%";
  4693. table.style.borderCollapse = "collapse";
  4694. const tableStyles = {
  4695. th: {
  4696. textAlign: "left",
  4697. padding: "8px 5px",
  4698. borderBottom: "1px solid #444",
  4699. fontSize: "14px",
  4700. fontWeight: "bold",
  4701. },
  4702. td: {
  4703. padding: "8px 5px",
  4704. borderBottom: "1px solid #333",
  4705. fontSize: "13px",
  4706. },
  4707. kbd: {
  4708. background: "#f7f7f7",
  4709. color: "#000",
  4710. border: "1px solid #555",
  4711. borderRadius: "3px",
  4712. padding: "2px 5px",
  4713. fontSize: "12px",
  4714. fontFamily: "monospace",
  4715. },
  4716. };
  4717. const headerRow = document.createElement("tr");
  4718. const shortcutHeader = document.createElement("th");
  4719. shortcutHeader.textContent = "Shortcut";
  4720. Object.assign(shortcutHeader.style, tableStyles.th);
  4721. headerRow.appendChild(shortcutHeader);
  4722.  
  4723. const actionHeader = document.createElement("th");
  4724. actionHeader.textContent = "Action";
  4725. Object.assign(actionHeader.style, tableStyles.th);
  4726. headerRow.appendChild(actionHeader);
  4727.  
  4728. table.appendChild(headerRow);
  4729. const shortcuts = [
  4730. { keys: ["Ctrl", "F1"], action: "Open 8chanSS settings" },
  4731. { keys: ["Tab"], action: "Target Quick Reply text area" },
  4732. { keys: ["R"], action: "Refresh Thread (5 sec. cooldown)" },
  4733. { keys: ["Ctrl", "Q"], action: "Toggle Quick Reply" },
  4734. { keys: ["Ctrl", "Enter"], action: "Submit post" },
  4735. { keys: ["Escape"], action: "Clear QR textarea and hide all dialogs" },
  4736. { keys: ["ALT", "W"], action: "Watch Thread" },
  4737. { keys: ["SHIFT", "T"], action: "Toggle Quote Threading" },
  4738. { keys: ["SHIFT", "M1"], action: "Hide Thread in Catalog" },
  4739. { keys: ["CTRL", "UP/DOWN"], action: "Scroll between Your Replies" },
  4740. { keys: ["CTRL", "SHIFT", "UP/DOWN"], action: "Scroll between Replies to You" },
  4741. { keys: ["Ctrl", "B"], action: "Bold text" },
  4742. { keys: ["Ctrl", "I"], action: "Italic text" },
  4743. { keys: ["Ctrl", "U"], action: "Underline text" },
  4744. { keys: ["Ctrl", "S"], action: "Spoiler text" },
  4745. { keys: ["Ctrl", "D"], action: "Srz Bizniz text" },
  4746. { keys: ["Ctrl", "M"], action: "Moe text" },
  4747. { keys: ["Alt", "C"], action: "Code block" },
  4748. ];
  4749. shortcuts.forEach((shortcut) => {
  4750. const row = document.createElement("tr");
  4751. const shortcutCell = document.createElement("td");
  4752. Object.assign(shortcutCell.style, tableStyles.td);
  4753. shortcut.keys.forEach((key, index) => {
  4754. const kbd = document.createElement("kbd");
  4755. kbd.textContent = key;
  4756. Object.assign(kbd.style, tableStyles.kbd);
  4757. shortcutCell.appendChild(kbd);
  4758. if (index < shortcut.keys.length - 1) {
  4759. const plus = document.createTextNode(" + ");
  4760. shortcutCell.appendChild(plus);
  4761. }
  4762. });
  4763.  
  4764. row.appendChild(shortcutCell);
  4765. const actionCell = document.createElement("td");
  4766. actionCell.textContent = shortcut.action;
  4767. Object.assign(actionCell.style, tableStyles.td);
  4768. row.appendChild(actionCell);
  4769.  
  4770. table.appendChild(row);
  4771. });
  4772.  
  4773. container.appendChild(table);
  4774. const note = document.createElement("p");
  4775. note.textContent =
  4776. "Text formatting shortcuts work when text is selected or when inserting at cursor position.";
  4777. note.style.fontSize = "12px";
  4778. note.style.marginTop = "15px";
  4779. note.style.opacity = "0.7";
  4780. note.style.fontStyle = "italic";
  4781. container.appendChild(note);
  4782.  
  4783. return container;
  4784. }
  4785. if (link) {
  4786. let menu = await createSettingsMenu();
  4787. link.style.cursor = "pointer";
  4788. link.title = "Open 8chanSS settings";
  4789. link.addEventListener("click", async function (e) {
  4790. e.preventDefault();
  4791. let menu = await createSettingsMenu();
  4792. menu.style.display = menu.style.display === "none" ? "block" : "none";
  4793. });
  4794. }
  4795. async function shortcutsGloballyEnabled() {
  4796. return await getSetting("enableShortcuts");
  4797. }
  4798. const bbCodeCombinations = new Map([
  4799. ["s", ["[spoiler]", "[/spoiler]"]],
  4800. ["b", ["'''", "'''"]],
  4801. ["u", ["__", "__"]],
  4802. ["i", ["''", "''"]],
  4803. ["d", ["==", "=="]],
  4804. ["m", ["[moe]", "[/moe]"]],
  4805. ["c", ["[code]", "[/code]"]],
  4806. ]);
  4807. function applyBBCode(textBox, key) {
  4808. const [openTag, closeTag] = bbCodeCombinations.get(key);
  4809. const { selectionStart, selectionEnd, value } = textBox;
  4810.  
  4811. if (selectionStart === selectionEnd) {
  4812. const before = value.slice(0, selectionStart);
  4813. const after = value.slice(selectionEnd);
  4814. const newCursor = selectionStart + openTag.length;
  4815. textBox.value = before + openTag + closeTag + after;
  4816. textBox.selectionStart = textBox.selectionEnd = newCursor;
  4817. } else {
  4818. const before = value.slice(0, selectionStart);
  4819. const selected = value.slice(selectionStart, selectionEnd);
  4820. const after = value.slice(selectionEnd);
  4821. textBox.value = before + openTag + selected + closeTag + after;
  4822. textBox.selectionStart = selectionStart + openTag.length;
  4823. textBox.selectionEnd = selectionEnd + openTag.length;
  4824. }
  4825. }
  4826. let lastHighlighted = null;
  4827. let lastType = null;
  4828. let lastRefreshTime = 0;
  4829.  
  4830. function getEligiblePostCells(isOwnReply) {
  4831. const selector = isOwnReply
  4832. ? '.postCell:has(a.youName), .opCell:has(a.youName)'
  4833. : '.postCell:has(a.quoteLink.you), .opCell:has(a.quoteLink.you)';
  4834. return Array.from(document.querySelectorAll(selector));
  4835. }
  4836.  
  4837. function scrollToReply(isOwnReply = true, getNextReply = true) {
  4838. const postCells = getEligiblePostCells(isOwnReply);
  4839. if (!postCells.length) return;
  4840. let currentIndex = -1;
  4841. const expectedType = isOwnReply ? "own" : "reply";
  4842. if (
  4843. lastType === expectedType &&
  4844. lastHighlighted
  4845. ) {
  4846. const container = lastHighlighted.closest('.postCell, .opCell');
  4847. currentIndex = postCells.indexOf(container);
  4848. }
  4849. if (currentIndex === -1) {
  4850. const viewportMiddle = window.innerHeight / 2;
  4851. currentIndex = postCells.findIndex(cell => {
  4852. const rect = cell.getBoundingClientRect();
  4853. return rect.top + rect.height / 2 > viewportMiddle;
  4854. });
  4855. if (currentIndex === -1) {
  4856. currentIndex = getNextReply ? -1 : postCells.length;
  4857. }
  4858. }
  4859. const targetIndex = getNextReply ? currentIndex + 1 : currentIndex - 1;
  4860. if (targetIndex < 0 || targetIndex >= postCells.length) return;
  4861.  
  4862. const postContainer = postCells[targetIndex];
  4863. if (postContainer) {
  4864. postContainer.scrollIntoView({ behavior: "smooth", block: "center" });
  4865. if (lastHighlighted) {
  4866. lastHighlighted.classList.remove('target-highlight');
  4867. }
  4868. let anchorId = null;
  4869. let anchorElem = postContainer.querySelector('[id^="p"]');
  4870. if (anchorElem && anchorElem.id) {
  4871. anchorId = anchorElem.id;
  4872. } else if (postContainer.id) {
  4873. anchorId = postContainer.id;
  4874. }
  4875. if (anchorId && location.hash !== '#' + anchorId) {
  4876. history.replaceState(null, '', '#' + anchorId);
  4877. }
  4878. const innerPost = postContainer.querySelector('.innerPost');
  4879. if (innerPost) {
  4880. innerPost.classList.add('target-highlight');
  4881. lastHighlighted = innerPost;
  4882. } else {
  4883. lastHighlighted = null;
  4884. }
  4885. lastType = isOwnReply ? "own" : "reply";
  4886. }
  4887. }
  4888. window.addEventListener('hashchange', () => {
  4889. if (lastHighlighted) {
  4890. lastHighlighted.classList.remove('target-highlight');
  4891. lastHighlighted = null;
  4892. }
  4893. const hash = location.hash.replace('#', '');
  4894. if (hash) {
  4895. const postElem = document.getElementById(hash);
  4896. if (postElem) {
  4897. const innerPost = postElem.querySelector('.innerPost');
  4898. if (innerPost) {
  4899. innerPost.classList.add('target-highlight');
  4900. lastHighlighted = innerPost;
  4901. }
  4902. }
  4903. }
  4904. });
  4905. document.addEventListener("keydown", async function (event) {
  4906. if (!(await shortcutsGloballyEnabled())) return;
  4907. const active = document.activeElement;
  4908. if (
  4909. active &&
  4910. event.key !== "Tab" &&
  4911. (active.tagName === "INPUT" ||
  4912. active.tagName === "TEXTAREA" ||
  4913. active.isContentEditable)
  4914. ) {
  4915. return;
  4916. }
  4917. if (event.ctrlKey && event.key === "F1") {
  4918. event.preventDefault();
  4919. let menu = document.getElementById("8chanSS-menu") || (await createSettingsMenu());
  4920. menu.style.display = menu.style.display === "none" || menu.style.display === "" ? "block" : "none";
  4921. return;
  4922. }
  4923. if (event.ctrlKey && (event.key === "q" || event.key === "Q")) {
  4924. event.preventDefault();
  4925. const hiddenDiv = document.getElementById("quick-reply");
  4926. if (!hiddenDiv) return;
  4927. const isHidden = hiddenDiv.style.display === "none" || hiddenDiv.style.display === "";
  4928. hiddenDiv.style.display = isHidden ? "block" : "none";
  4929. if (isHidden) {
  4930. setTimeout(() => {
  4931. const textarea = document.getElementById("qrbody");
  4932. if (textarea) textarea.focus();
  4933. }, 50);
  4934. }
  4935. return;
  4936. }
  4937. if (
  4938. event.key === "Tab" &&
  4939. !event.ctrlKey &&
  4940. !event.altKey &&
  4941. !event.metaKey
  4942. ) {
  4943. const qrbody = document.getElementById("qrbody");
  4944. const captcha = document.getElementById("QRfieldCaptcha");
  4945.  
  4946. if (qrbody) {
  4947. if (document.activeElement === qrbody && captcha) {
  4948. event.preventDefault();
  4949. captcha.focus();
  4950. } else if (document.activeElement === captcha) {
  4951. event.preventDefault();
  4952. qrbody.focus();
  4953. } else if (document.activeElement !== qrbody) {
  4954. event.preventDefault();
  4955. qrbody.focus();
  4956. }
  4957. }
  4958. return;
  4959. }
  4960. if (event.key === "r" || event.key === "R") {
  4961. const isThread = window.pageType?.isThread;
  4962. const isCatalog = window.pageType?.isCatalog;
  4963. const threadRefreshBtn = document.getElementById("refreshButton");
  4964. const catalogRefreshBtn = document.getElementById("catalogRefreshButton");
  4965. const now = Date.now();
  4966.  
  4967. if (
  4968. (isThread && threadRefreshBtn) ||
  4969. (isCatalog && catalogRefreshBtn)
  4970. ) {
  4971. if (now - lastRefreshTime >= 5000) {
  4972. event.preventDefault();
  4973. if (isThread && threadRefreshBtn) {
  4974. threadRefreshBtn.click();
  4975. } else if (isCatalog && catalogRefreshBtn) {
  4976. catalogRefreshBtn.click();
  4977. }
  4978. lastRefreshTime = now;
  4979. } else {
  4980. event.preventDefault();
  4981. }
  4982. return;
  4983. }
  4984. }
  4985. if (event.shiftKey && !event.ctrlKey && !event.altKey && (event.key === "t" || event.key === "T")) {
  4986. event.preventDefault();
  4987.  
  4988. const current = await getSetting("quoteThreading");
  4989. const newValue = !current;
  4990. await setSetting("quoteThreading", newValue);
  4991. try {
  4992. const msg = `Quote threading <b>${newValue ? "enabled" : "disabled"}</b>`;
  4993. const color = newValue ? 'blue' : 'black';
  4994. callPageToast(msg, color, 1300);
  4995. } catch { }
  4996. setTimeout(() => window.location.reload(), 1400);
  4997. return;
  4998. }
  4999. if (event.key === "Escape") {
  5000. const textarea = document.getElementById("qrbody");
  5001. if (textarea) textarea.value = "";
  5002. const quickReply = document.getElementById("quick-reply");
  5003. if (quickReply) quickReply.style.display = "none";
  5004. const threadWatcher = document.getElementById("watchedMenu");
  5005. if (threadWatcher) threadWatcher.style.display = "none";
  5006. return;
  5007. }
  5008. if (event.ctrlKey && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
  5009. event.preventDefault();
  5010. const isOwnReply = !event.shiftKey;
  5011. const isNext = event.key === 'ArrowDown';
  5012. scrollToReply(isOwnReply, isNext);
  5013. return;
  5014. }
  5015. if (
  5016. event.altKey &&
  5017. (event.key === "w" || event.key === "W")
  5018. ) {
  5019. event.preventDefault();
  5020. const btn = document.querySelector(".watchButton");
  5021. if (btn && !btn.classList.contains("watched-active")) {
  5022. btn.click();
  5023. setTimeout(() => {
  5024. btn.classList.add("watched-active");
  5025. }, 100);
  5026. }
  5027. return;
  5028. }
  5029. });
  5030. const replyTextarea = document.getElementById("qrbody");
  5031. if (!(await shortcutsGloballyEnabled())) {
  5032. return;
  5033. } else if (replyTextarea) {
  5034. replyTextarea.addEventListener("keydown", async function (event) {
  5035. if (event.ctrlKey && event.key === "Enter") {
  5036. event.preventDefault();
  5037. const submitButton = document.getElementById("qrbutton");
  5038. if (submitButton) {
  5039. submitButton.click();
  5040. if (await getSetting("watchThreadOnReply")) {
  5041. setTimeout(() => {
  5042. const btn = document.querySelector(".watchButton");
  5043. if (btn && !btn.classList.contains("watched-active")) {
  5044. btn.click();
  5045. setTimeout(() => {
  5046. btn.classList.add("watched-active");
  5047. }, 100);
  5048. }
  5049. }, 500);
  5050. }
  5051. }
  5052. }
  5053. });
  5054. replyTextarea.addEventListener("keydown", function (event) {
  5055. const key = event.key.toLowerCase();
  5056. if (key === "c" && event.altKey && !event.ctrlKey && bbCodeCombinations.has(key)) {
  5057. event.preventDefault();
  5058. applyBBCode(event.target, key);
  5059. return;
  5060. }
  5061. if (event.ctrlKey && !event.altKey && bbCodeCombinations.has(key) && key !== "c") {
  5062. event.preventDefault();
  5063. applyBBCode(event.target, key);
  5064. return;
  5065. }
  5066. });
  5067. }
  5068. function featureCatalogHiding() {
  5069. const STORAGE_KEY = "8chanSS_hiddenCatalogThreads";
  5070. let showHiddenMode = false;
  5071. function getBoardAndThreadNumFromCell(cell) {
  5072. const link = cell.querySelector("a.linkThumb[href*='/res/']");
  5073. if (!link) return { board: null, threadNum: null };
  5074. const match = link.getAttribute("href").match(/^\/([^/]+)\/res\/(\d+)\.html/);
  5075. if (!match) return { board: null, threadNum: null };
  5076. return { board: match[1], threadNum: match[2] };
  5077. }
  5078. async function loadHiddenThreadsObj() {
  5079. const raw = await GM.getValue(STORAGE_KEY, "{}");
  5080. try {
  5081. const obj = JSON.parse(raw);
  5082. return typeof obj === "object" && obj !== null ? obj : {};
  5083. } catch {
  5084. return {};
  5085. }
  5086. }
  5087. async function saveHiddenThreadsObj(obj) {
  5088. await GM.setValue(STORAGE_KEY, JSON.stringify(obj));
  5089. }
  5090. async function applyHiddenThreads() {
  5091. const hiddenThreadsObjRaw = await GM.getValue(STORAGE_KEY, "{}");
  5092. let hiddenThreadsObj;
  5093. try {
  5094. hiddenThreadsObj = JSON.parse(hiddenThreadsObjRaw);
  5095. if (typeof hiddenThreadsObj !== "object" || hiddenThreadsObj === null) hiddenThreadsObj = {};
  5096. } catch {
  5097. hiddenThreadsObj = {};
  5098. }
  5099. document.querySelectorAll(".catalogCell").forEach(cell => {
  5100. const { board, threadNum } = getBoardAndThreadNumFromCell(cell);
  5101. if (!board || !threadNum) return;
  5102. const hiddenThreads = hiddenThreadsObj[board] || [];
  5103.  
  5104. if (typeof showHiddenMode !== "undefined" && showHiddenMode) {
  5105. if (hiddenThreads.includes(threadNum)) {
  5106. cell.style.display = "";
  5107. cell.classList.add("ss-unhide-thread");
  5108. cell.classList.remove("ss-hidden-thread");
  5109. } else {
  5110. cell.style.display = "none";
  5111. cell.classList.remove("ss-unhide-thread", "ss-hidden-thread");
  5112. }
  5113. } else {
  5114. if (hiddenThreads.includes(threadNum)) {
  5115. cell.style.display = "none";
  5116. cell.classList.add("ss-hidden-thread");
  5117. cell.classList.remove("ss-unhide-thread");
  5118. } else {
  5119. cell.style.display = "";
  5120. cell.classList.remove("ss-hidden-thread", "ss-unhide-thread");
  5121. }
  5122. }
  5123. });
  5124. }
  5125. async function onCatalogCellClick(e) {
  5126. const cell = e.target.closest(".catalogCell");
  5127. if (!cell) return;
  5128. if (e.shiftKey && e.button === 0) {
  5129. const { board, threadNum } = getBoardAndThreadNumFromCell(cell);
  5130. if (!board || !threadNum) return;
  5131.  
  5132. let hiddenThreadsObj = await loadHiddenThreadsObj();
  5133. if (!hiddenThreadsObj[board]) hiddenThreadsObj[board] = [];
  5134. let hiddenThreads = hiddenThreadsObj[board];
  5135.  
  5136. if (showHiddenMode) {
  5137. hiddenThreads = hiddenThreads.filter(num => num !== threadNum);
  5138. hiddenThreadsObj[board] = hiddenThreads;
  5139. await saveHiddenThreadsObj(hiddenThreadsObj);
  5140. await applyHiddenThreads();
  5141. } else {
  5142. if (!hiddenThreads.includes(threadNum)) {
  5143. hiddenThreads.push(threadNum);
  5144. hiddenThreadsObj[board] = hiddenThreads;
  5145. }
  5146. await saveHiddenThreadsObj(hiddenThreadsObj);
  5147. cell.style.display = "none";
  5148. cell.classList.add("ss-hidden-thread");
  5149. }
  5150. e.preventDefault();
  5151. e.stopPropagation();
  5152. }
  5153. }
  5154. async function showAllHiddenThreads() {
  5155. showHiddenMode = true;
  5156. await applyHiddenThreads();
  5157. const btn = document.getElementById("ss-show-hidden-btn");
  5158. if (btn) btn.textContent = "Hide Hidden";
  5159. }
  5160. async function hideAllHiddenThreads() {
  5161. showHiddenMode = false;
  5162. await applyHiddenThreads();
  5163. const btn = document.getElementById("ss-show-hidden-btn");
  5164. if (btn) btn.textContent = "Show Hidden";
  5165. }
  5166. async function toggleShowHiddenThreads() {
  5167. if (showHiddenMode) {
  5168. await hideAllHiddenThreads();
  5169. } else {
  5170. await showAllHiddenThreads();
  5171. }
  5172. }
  5173. function addShowHiddenButton() {
  5174. if (document.getElementById("ss-show-hidden-btn")) return;
  5175. const refreshBtn = document.querySelector("#catalogRefreshButton");
  5176. if (!refreshBtn) return;
  5177. const btn = document.createElement("button");
  5178. btn.id = "ss-show-hidden-btn";
  5179. btn.className = "catalogLabel";
  5180. btn.type = "button";
  5181. btn.textContent = "Show Hidden";
  5182. btn.style.marginRight = "8px";
  5183. btn.addEventListener("click", toggleShowHiddenThreads);
  5184. refreshBtn.parentNode.insertBefore(btn, refreshBtn);
  5185. }
  5186. function hideThreadsOnRefresh() {
  5187. if (!window.pageType?.isCatalog) return;
  5188. onReady(addShowHiddenButton);
  5189. onReady(applyHiddenThreads);
  5190. const catalogContainer = document.querySelector(".catalogWrapper, .catalogDiv");
  5191. if (catalogContainer) {
  5192. catalogContainer.addEventListener("click", onCatalogCellClick, true);
  5193. }
  5194. }
  5195. hideThreadsOnRefresh();
  5196. const catalogDivObs = observeSelector('.catalogDiv', { childList: true, subtree: false });
  5197. if (catalogDivObs) {
  5198. const debouncedApply = debounce(applyHiddenThreads, 50);
  5199. catalogDivObs.addHandler(function catalogHidingHandler() {
  5200. debouncedApply();
  5201. });
  5202. }
  5203. }
  5204. (function noCaptchaHistory() {
  5205. const captchaInput = document.getElementById("QRfieldCaptcha");
  5206. if (captchaInput) {
  5207. captchaInput.autocomplete = "off";
  5208. }
  5209. })();
  5210. function preventFooterScrollIntoView() {
  5211. const footer = document.getElementById('footer');
  5212. if (footer && !footer._scrollBlocked) {
  5213. footer._scrollBlocked = true;
  5214. footer.scrollIntoView = function () {
  5215. return;
  5216. };
  5217. }
  5218. }
  5219. (function moveFileUploadsBelowOp() {
  5220. if (window.pageType?.isCatalog) {
  5221. return;
  5222. } else if (opHeadTitle && innerOP) {
  5223. innerOP.insertBefore(opHeadTitle, innerOP.firstChild);
  5224. }
  5225. })();
  5226. document.addEventListener('click', function (e) {
  5227. const a = e.target.closest('.panelBacklinks > a');
  5228. if (a) {
  5229. setTimeout(() => {
  5230. a.classList.toggle('reply-inlined');
  5231. }, 0);
  5232. return;
  5233. }
  5234. const b = e.target.closest('a.quoteLink');
  5235. if (b) {
  5236. setTimeout(() => {
  5237. b.classList.toggle('quote-inlined');
  5238. }, 0);
  5239. }
  5240. });
  5241. async function updateNotif() {
  5242. const VERSION_KEY = "8chanSS_version";
  5243. let storedVersion = null;
  5244. try {
  5245. storedVersion = await GM.getValue(VERSION_KEY, null);
  5246. } catch (err) {
  5247. console.error("[8chanSS] Failed to get stored script version:", err);
  5248. }
  5249.  
  5250. if (storedVersion !== VERSION) {
  5251. if (storedVersion !== null) {
  5252. let tries = 0;
  5253. while (typeof window.callPageToast !== "function" && tries < 20) {
  5254. await new Promise(res => setTimeout(res, 100));
  5255. tries++;
  5256. }
  5257. if (typeof window.callPageToast === "function") {
  5258. window.callPageToast(
  5259. `8chanSS has updated to v${VERSION}. Check out the <b><a href="https://github.com/otacoo/8chanSS/blob/main/CHANGELOG.md" target="_blank">changelog</a></b>.`,
  5260. "blue",
  5261. 15000
  5262. );
  5263. }
  5264. }
  5265. try {
  5266. await GM.setValue(VERSION_KEY, VERSION);
  5267. } catch (err) {
  5268. console.error("[8chanSS] Failed to store script version:", err);
  5269. }
  5270. }
  5271. }
  5272. });
Add Comment
Please, Sign In to add comment