Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name 8chanSS
- // @version 1.52.1
- // @namespace 8chanss
- // @description A userscript to add functionality to 8chan.
- // @author otakudude
- // @minGMVer 4.3
- // @minFFVer 121
- // @license MIT; https://github.com/otacoo/8chanSS/blob/main/LICENSE
- // @match *://8chan.moe/*
- // @match *://8chan.se/*
- // @exclude *://8chan.moe/login.html
- // @exclude *://8chan.se/login.html
- // @grant GM.getValue
- // @grant GM.setValue
- // @grant GM.deleteValue
- // @grant GM.listValues
- // @grant GM.xmlHttpRequest
- // @connect youtube.com
- // @connect i.ytimg.com
- // @run-at document-start
- // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAZlBMVEUAAABdlloAhl758AH58AH58AcAhl758ADj4CYAh14AhV4AhV0Ahl748AcChl4Chl0Ab2H58AIAhl758AD58AAAhl757wL48AD47wL78QL47wcAh1748AF3oFfs5yEAh1/68QDz7BM5qSu8AAAAH3RSTlMA/lg/OYtM8g/onXtoXzAaCdzBsIFzczMeaCXXyrmp9ddA3QAAANpJREFUSMft0tkOgjAQheFjtVCQVVxwnfr+L+kWM5FOC73TxP/6fBedFJwpyx5CtSpqSHXWpns4qYxo1cDtkNp7GoOW9KgSwM4+09KeEhmw4H0IuGJDAbCw79a8nwJYFDQCuO1gT8oLWCiKAXavKA5cZ78I5n/wBx7wfb+1TwOggpD2gxxSpvWBrIbY3AcUPK1lkMNbJ4FV4wd964KsQqBF6oAEwcoh2GAk/QlyjNYx4AeHMicGxxoTOrRvIB5IPtULJJhY+QIFJrd9gCUi0tdZjqgu5yYOGAO5G/kyc3TkciPeAAAAAElFTkSuQmCC
- // @downloadURL https://update.greasyfork.org/scripts/533268/8chanSS.user.js
- // @updateURL https://update.greasyfork.org/scripts/533268/8chanSS.meta.js
- // ==/UserScript==
- function onReady(fn) {
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", fn, { once: true });
- } else {
- fn();
- }
- }
- const debounce = (fn, delay) => {
- let timeout;
- return (...args) => {
- clearTimeout(timeout);
- timeout = setTimeout(() => fn.apply(this, args), delay);
- };
- };
- const observerRegistry = {};
- function observeSelector(selector, options = { childList: true, subtree: false }) {
- if (observerRegistry[selector]) return observerRegistry[selector];
- const node = document.querySelector(selector);
- if (!node) return null;
- const handlers = [];
- const observer = new MutationObserver(mutations => {
- for (const handler of handlers) {
- try {
- handler(mutations, node);
- } catch (e) {
- console.error(`Observer handler error for ${selector}:`, e);
- }
- }
- });
- observer.observe(node, options);
- window.addEventListener('beforeunload', () => observer.disconnect());
- observerRegistry[selector] = {
- node,
- observer,
- handlers,
- addHandler: fn => handlers.push(fn)
- };
- return observerRegistry[selector];
- }
- window.pageType = (() => {
- const path = window.location.pathname.toLowerCase();
- const currentHost = window.location.hostname.toLowerCase();
- return {
- isCatalog: /\/catalog\.html$/i.test(path),
- isThread: /\/(res|last)\/[^/]+\.html$/i.test(path),
- isLast: /\/last\/[^/]+\.html$/i.test(path),
- isIndex: /\/[^/]+\/$/i.test(path),
- is8chan: /^8chan\.(se|moe)$/.test(currentHost),
- host: currentHost,
- path: path
- };
- })();
- (function injectCssAsap() {
- function doInject() {
- if (document.getElementById('8chSShim')) return;
- if (!document.head) {
- setTimeout(doInject, 1);
- return;
- }
- const style = document.createElement('style');
- style.id = '8chSShim';
- 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}";
- document.head.appendChild(style);
- }
- doInject();
- })();
- onReady(async function () {
- "use strict";
- const divThreads = document.getElementById('divThreads');
- const innerOP = document.querySelector('.innerOP');
- const divPosts = document.querySelector('.divPosts');
- const opHeadTitle = document.querySelector('.opHead.title');
- const catalogDiv = document.querySelector('.catalogDiv');
- const VERSION = "1.52.1";
- const scriptSettings = {
- site: {
- _siteTWTitle: { type: "title", label: ":: Thread Watcher" },
- _siteSection1: { type: "separator" },
- alwaysShowTW: {
- label: "Pin Thread Watcher",
- default: false,
- subOptions: {
- noPinInCatalog: {
- label: "Don't pin in Catalog",
- default: false,
- }
- }
- },
- autoExpandTW: { label: "Auto Expand Thread Watcher", default: false },
- _siteSiteTitle: { type: "title", label: ":: Site" },
- _siteSection2: { type: "separator" },
- customFavicon: {
- label: "Custom Favicon",
- default: false,
- subOptions: {
- faviconStyle: {
- label: "Favicon Style",
- type: "select",
- default: "default",
- options: [
- { value: "default", label: "Default" },
- { value: "pixel", label: "Pixel" },
- { value: "pixel_alt", label: "Pixel Alt" },
- { value: "eight", label: "Eight" },
- { value: "eight_dark", label: "Eight Dark" }
- ]
- }
- }
- },
- enableBottomHeader: { label: "Bottom Header", default: false },
- enableAutoHideHeaderScroll: { label: "Auto-hide Header On Scroll", default: false },
- enableHeaderCatalogLinks: {
- label: "Header Catalog Links",
- default: true,
- subOptions: {
- openInNewTab: {
- label: "Always open in new tab",
- default: false,
- }
- }
- },
- enableScrollArrows: { label: "Show Up/Down Arrows", default: false },
- _siteMediaTitle: { type: "title", label: ":: Media" },
- _siteSection3: { type: "separator" },
- enableThreadImageHover: { label: "Thread Image Hover", default: true },
- blurSpoilers: {
- label: "Blur Spoilers",
- default: false,
- subOptions: {
- removeSpoilers: {
- label: "Remove Spoilers",
- default: false
- }
- }
- },
- enableMediaViewer: {
- label: "Enable Advanced Media Viewer",
- default: false,
- subOptions: {
- viewerStyle: {
- label: "Style",
- type: "select",
- default: "native",
- options: [
- { value: "native", label: "Native" },
- { value: "topright", label: "Pin Top Right" },
- { value: "topleft", label: "Pin Top Left" }
- ]
- }
- }
- },
- hoverVideoVolume: { label: "Hover Media Volume (0-100%)", default: 50, type: "number", min: 0, max: 100 }
- },
- threads: {
- _threadsNotiTitle: { type: "title", label: ":: Notifications" },
- _threadsSection1: { type: "separator" },
- beepOnYou: { label: "Beep on (You)", default: false },
- notifyOnYou: {
- label: "Tab Notification when (You) (!)",
- default: true,
- subOptions: {
- customMessage: {
- label: "Custom Text (max: 8 chars.)",
- default: "",
- type: "text",
- maxLength: 9
- }
- }
- },
- _threadsNavTitle: { type: "title", label: ":: Navigation & Others" },
- _threadsSection3: { type: "separator" },
- enableScrollSave: {
- label: "Save Scroll Position",
- default: true,
- subOptions: {
- showUnreadLine: {
- label: "Show Unread Line",
- default: true,
- }
- }
- },
- quoteThreading: { label: "Quote Threading", default: false },
- enableHashNav: { label: "Hash Navigation", default: false },
- threadStatsInHeader: { label: "Thread Stats in Header", default: false },
- watchThreadOnReply: { label: "Watch Thread on Reply", default: true },
- scrollToBottom: { label: "Don't Scroll to Bottom on Reply", default: true }
- },
- catalog: {
- enableCatalogImageHover: { label: "Catalog Image Hover", default: true },
- enableThreadHiding: { label: "Enable Thread Hiding", default: false },
- openCatalogThreadNewTab: { label: "Always Open Threads in New Tab", default: false },
- enableLastFifty: { label: "Show Last 50 Posts button", default: false }
- },
- styling: {
- _stylingSiteTitle: { type: "title", label: ":: Site Styling" },
- _stylingSection1: { type: "separator" },
- hideAnnouncement: { label: "Hide Announcement", default: false },
- hidePanelMessage: { label: "Hide Panel Message", default: false },
- hidePostingForm: {
- label: "Hide Posting Form",
- default: false,
- subOptions: {
- showCatalogForm: {
- label: "Don't Hide in Catalog",
- default: false
- }
- }
- },
- hideBanner: { label: "Hide Board Banners", default: false },
- hideDefaultBL: { label: "Hide Default Board List", default: true },
- hideNoCookieLink: { label: "Hide No Cookie? Link", default: false },
- hideJannyTools: { label: "Hide Janitor Forms", default: false },
- _stylingThreadTitle: { type: "title", label: ":: Thread Styling" },
- _stylingSection2: { type: "separator" },
- highlightOnYou: { label: "Style (You) posts", default: true },
- opBackground: { label: "OP background", default: false },
- enableStickyQR: { label: "Sticky Quick Reply", default: false },
- fadeQuickReply: { label: "Fade Quick Reply", default: false },
- enableFitReplies: { label: "Fit Replies", default: false },
- enableSidebar: {
- label: "Enable Sidebar",
- default: false,
- subOptions: {
- leftSidebar: {
- label: "Sidebar on Left",
- default: false
- }
- }
- },
- threadHideCloseBtn: { label: "Hide Inline Close Button", default: false },
- hideCheckboxes: { label: "Hide Post Checkbox", default: false }
- },
- miscel: {
- enableShortcuts: { label: "Enable Keyboard Shortcuts", type: "checkbox", default: true },
- enableUpdateNotif: { label: "8chanSS update notifications", default: true },
- enhanceYoutube: {
- label: "Enhanced Youtube Links",
- default: true,
- subOptions: {
- ytThumbs: {
- label: "Show Thumbnails on Hover",
- default: true,
- }
- }
- },
- switchTimeFormat: { label: "Enable 12-hour Clock (AM/PM)", default: false },
- truncFilenames: {
- label: "Truncate filenames",
- default: false,
- subOptions: {
- customTrunc: {
- label: "Max filename length (min: 5, max: 50)",
- default: 15,
- type: "number",
- min: 5,
- max: 50
- }
- }
- },
- enableTheSauce: {
- label: "Sauce Links",
- default: false,
- subOptions: {
- iqdb: {
- label: "IQDB",
- default: false,
- },
- saucenao: {
- label: "Saucenao",
- default: false,
- },
- pixiv: {
- label: "Pixiv (only added if filename matches Pixiv ID)",
- default: false,
- }
- }
- },
- _miscelFilterTitle: { type: "title", label: ":: Filtering" },
- _miscelSection1: { type: "separator" },
- enableHidingMenu: {
- label: "Alternative post hiding menu & features",
- default: false,
- subOptions: {
- recursiveHide: {
- label: "Recursive hide/filter/name+ (hide replies to replies)",
- default: false
- }
- }
- },
- hideHiddenPostStub: { label: "Hide Stubs of Hidden Posts", default: false, },
- _miscelIDTitle: { type: "title", label: ":: IDs" },
- _miscelSection2: { type: "separator" },
- highlightNewIds: {
- label: "Highlight New IDs",
- default: false,
- subOptions: {
- idHlStyle: {
- label: "Highlight Style",
- type: "select",
- default: "moetext",
- options: [
- { value: "moetext", label: "Moe" },
- { value: "glow", label: "Glow" },
- { value: "dotted", label: "Border" }
- ]
- }
- }
- },
- alwaysShowIdCount: { label: "Always show post count for IDs", default: false },
- enableIdFilters: {
- label: "Show only posts by ID when ID is clicked",
- type: "checkbox",
- default: true,
- subOptions: {
- showIdLinksOnly: {
- label: "Show as a floating list",
- default: false
- },
- showIdLinksVertical: {
- label: "Show posts in a vertical list",
- default: false
- }
- }
- },
- enableIdToggle: { label: "Add menu entry to toggle IDs as Yours", type: "checkbox", default: false }
- }
- };
- Object.freeze(scriptSettings);
- function flattenSettings() {
- const result = {};
- Object.keys(scriptSettings).forEach((category) => {
- Object.keys(scriptSettings[category]).forEach((key) => {
- if (key.startsWith('_')) return;
- result[key] = scriptSettings[category][key];
- const subOptions = scriptSettings[category][key].subOptions;
- if (!subOptions || typeof subOptions !== "object") return;
- Object.keys(subOptions).forEach((subKey) => {
- const fullKey = `${key}_${subKey}`;
- result[fullKey] = subOptions[subKey];
- });
- });
- });
- return Object.freeze(result);
- }
- let flatSettings = flattenSettings();
- async function getSetting(key) {
- if (!flatSettings[key]) {
- console.warn(`Setting key not found: ${key}`);
- return false;
- }
- let val;
- try {
- val = await GM.getValue("8chanSS_" + key, null);
- } catch (err) {
- console.error(`Failed to get setting for key ${key}:`, err);
- return flatSettings[key]?.default ?? false;
- }
- if (val === null) return flatSettings[key].default;
- switch (flatSettings[key].type) {
- case "number":
- return Number(val);
- case "text":
- return String(val).replace(/[<>"']/g, "").slice(0, flatSettings[key].maxLength || 32);
- case "textarea":
- case "select":
- return String(val);
- default:
- return val === "true";
- }
- }
- async function setSetting(key, value) {
- try {
- await GM.setValue("8chanSS_" + key, String(value));
- } catch (err) {
- console.error(`Failed to set setting for key ${key}:`, err);
- }
- }
- (async function featureCssClassToggles() {
- document.documentElement.classList.add("8chanSS");
- const enableSidebar = await getSetting("enableSidebar");
- const enableSidebar_leftSidebar = await getSetting("enableSidebar_leftSidebar");
- const classToggles = {
- enableFitReplies: "fit-replies",
- enableSidebar_leftSidebar: "ss-leftsidebar",
- enableStickyQR: "sticky-qr",
- fadeQuickReply: "fade-qr",
- enableBottomHeader: "bottom-header",
- hideHiddenPostStub: "hide-stub",
- hideBanner: "disable-banner",
- hidePostingForm: "hide-posting-form",
- hidePostingForm_showCatalogForm: "show-catalog-form",
- hideDefaultBL: "hide-defaultBL",
- hidePanelMessage: "hide-panelmessage",
- highlightOnYou: "highlight-yous",
- threadHideCloseBtn: "hide-close-btn",
- hideCheckboxes: "hide-checkboxes",
- hideNoCookieLink: "hide-nocookie",
- autoExpandTW: "auto-expand-tw",
- hideJannyTools: "hide-jannytools",
- opBackground: "op-background",
- blurSpoilers: "ss-blur-spoilers"
- };
- if (enableSidebar && !enableSidebar_leftSidebar) {
- document.documentElement.classList.add("ss-sidebar");
- } else {
- document.documentElement.classList.remove("ss-sidebar");
- }
- const settingKeys = Object.keys(classToggles);
- const settingValues = await Promise.all(settingKeys.map(getSetting));
- settingKeys.forEach((key, i) => {
- const className = classToggles[key];
- if (settingValues[i]) {
- document.documentElement.classList.add(className);
- } else {
- document.documentElement.classList.remove(className);
- }
- });
- if (window.pageType?.isCatalog) {
- document.documentElement.classList.add("is-catalog");
- } else {
- document.documentElement.classList.remove("is-catalog");
- }
- if (window.pageType?.isThread) {
- document.documentElement.classList.add("is-thread");
- } else {
- document.documentElement.classList.remove("is-thread");
- }
- if (window.pageType?.isIndex) {
- document.documentElement.classList.add("is-index");
- } else {
- document.documentElement.classList.remove("is-index");
- }
- })();
- (async function featureSidebar() {
- const enableSidebar = await getSetting("enableSidebar");
- const enableSidebar_leftSidebar = await getSetting("enableSidebar_leftSidebar");
- const mainPanel = document.getElementById("mainPanel");
- if (!mainPanel) return;
- if (enableSidebar && enableSidebar_leftSidebar) {
- mainPanel.style.marginLeft = "19rem";
- mainPanel.style.marginRight = "0";
- } else if (enableSidebar) {
- mainPanel.style.marginRight = "19rem";
- mainPanel.style.marginLeft = "0";
- } else {
- mainPanel.style.marginRight = "0";
- mainPanel.style.marginLeft = "0";
- }
- })();
- (function injectCustomCss() {
- if (document.getElementById('8chSS')) return;
- let css = "";
- if (window.pageType?.is8chan) {
- 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}";
- }
- if (window.pageType?.isThread) {
- 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}";
- } else if (window.pageType?.isCatalog) {
- 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}";
- }
- if (!css) return;
- const style = document.createElement('style');
- style.id = '8chSS';
- style.textContent = css;
- document.head.appendChild(style);
- })();
- const faviconManager = (() => {
- const STYLES = [
- "default",
- "eight", "eight_dark",
- "pixel", "pixel_alt"
- ];
- const STATES = ["base", "unread", "notif"];
- const FAVICON_DATA = {
- default: {
- 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",
- 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=",
- 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=",
- },
- eight: {
- 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==",
- unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAtFBMVEUAAAAAYYUAYYQAXIAAW38AYYMAY4YAXYAAYIQAZIcAY4Udc5MAWX8BZogldJQAWn6L2fIRa4xy0faB2/xt0vqI3Ph+0vBdxO1Wp8c/i6l41fhPwO1Ivu1Lo8dEoMYegaYYcZMEa46X5f1NxvZfyfNZyPOI1OxqwuV3w947rttputpdttldq8porscrmcNRnbtEmLpOmLU9krQFb5MHZIVIueY6tuaAyeJOstp1vNRCp9Auh6qJsLovAAAADnRSTlMAWfLRzujVm5EzHAqLiBrNc7IAAADDSURBVBjTNY9XEsIwEEPd0mFtp4d0eocU+v3vRRic9yeNZldCCtsgIBlGI5aWbMX6JE2lHdoEgRDzh1QZDGGYALzmJ/Y3JhfvyHUCYi1/eWzS2Iv5VAchYLhH4ZpvZvue8yTYEmRrbeT7rjvbx8cwSAxklMuoKA6D43lho9mI7FYl5/3B3cQXoBZCkKW1PuWFn7MJdoYf9T3riM5z/6pasvPiXVWfKKrGknK3SNPVsqWO2mHK8y17lpqFRjCrO2LYSn0BV4wQhK3VgFcAAAAASUVORK5CYII=",
- 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=",
- },
- eight_dark: {
- 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",
- 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=",
- 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",
- },
- pixel: {
- base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAAAAAAAABiam00WmxcZWlFTE4jQE1GTU8jQU4yV2g8QUIbNkE/WGRWYWY9WGUAAAAAAADg8vnY6fB5yOyA0fW95fi23fB+z/PQ4Od0weNoeYFWcoBSXmNFS00jP0wFxP2oAAAAEnRSTlMAIRHe3v3w8Nzc/ezs/u7u7EbfERAsAAAAcUlEQVQY05VP1wqAMAw0de82TYf7//9SOhBFELy3G1wuyQcYYw8OVVHBzYJUokzBW3VZg+MYFMhI0ThMMm9zlIXnVmmzbFy0iHtSkmq63mhtYuJQthO8X40OHSHR0DwO4UrsoAziDq84HudcO15P/MAJxpAGt0Xg7i0AAAAASUVORK5CYII=",
- unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAATHcAIGJii6I0gKFFepYjdJVGepYjdJUAXIFeiaEyf6A8dZIbcJJThZ8/f55WhZ89f58AY4YAM2ng8/p/0vbY6/R5zPC95vq23/TQ5O10xelolKxWj6tFepUjdJUJarSxAAAAFHRSTlMAIBHe3vDw3Nwj/f3s7P7+7u7sRkWW9w4AAAB4SURBVBjTlY/ZDoUwCERL3fcNSnsX/f+/tEpjNCYm8nZgMjOoh9FaXxiyJIPTCSKDJoL9lKc5bIx+U3iOmXjoP6asSzSJKjwTWfdrx6lGnFXKVDWds9a1oliImmns/s6Khygq/g69pAQPjiH0kBTPoc7R4/bEi1kBHCgHTIPhydUAAAAASUVORK5CYII=",
- notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAhFBMVEUAAAAAV34AIDg0gKEkdJQjdJUUAAMAAEAyf6AbcJI7dZA/f55MAABAgaBEeZVljaVUhJ4AZIZpkKYAQmp5y/B+0fVrS0224PPf8vnP4uu85vqA0/fY6/NUIivtAABNHynjAAB0xelolKxWj6tNgZpFepUjdJVOTU9fREtAREvaAABiAADosgfnAAAAFHRSTlMAIBHe8NzcIP3s3v7x7u7e7uzeRqhOt1sAAACESURBVBjTlY9ZDoMwDERxCHvp6mASSKH7dv/71YWoAiEh4b83Ho3H3swIIUYchaswGqwgta1N4ccQyABgr4tjoTfA7KPCfKtrFsp63bGqDJ2vloXy5klUcZKRMfRsO8e9Uslhl12IXp8HZ/SOGE+5bN4NX3EZ6IPr0SvMrs6/x+SJBfMF/doItAPX5RIAAAAASUVORK5CYII=",
- },
- pixel_alt: {
- base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAY1BMVEUAAAAATHcAIGJii6I0gKFFepYjdJVGepYjdJUAXIFeiaEyf6A8dZIbcJJThZ8/f55WhZ89f58AY4YAQmoAJGjg8/p/0vbY6/R5zPC95vq23/TQ5O10xelolKxWj6tFepUjdJUOfB3LAAAAFXRSTlMAIBHe3vDw3Nwj/f3s7P7+7u7sRkaqQLDYAAAAeUlEQVQY05WP6Q6EMAiES73vYxdKu4e+/1NapTEaExP598FkZlA3o7U+MWRJBocTRAZNBNspT3NYGf2m8Bwz8dB/TFmXaBJVeCay7teO7xpxUilT1XTOWteKYiZqXmP3d1Y8RFHxd+glJXhwDKGHpHgOdfYelycezAI9gweHH0yPzQAAAABJRU5ErkJggg==",
- unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAhFBMVEUAAAAjR1sKChFJeZBBa4FAZ3tCbIEVFRYABA9IeI9OaXlReI1MTExSeo9WcYBxh5RjfYw4WGt0ipcdKTh7y+6A0vdqbm/f8fi85fl+0PTY6vK23fDt7OzP4OdVWFlOUlS44PPj4+NZdITR4+t3xefa2tpyj55jiZ1Ba4BgZ2tiYF9ATFKjqzLCAAAAFHRSTlMAIBHe8O3c3CD93v7x7u7e7uzeRvBdAgMAAACFSURBVBjTlY9ZDoMwDERxIKylqwOBlATo3t7/fjVpVIGQkPDfG4/GY29hGGMTjqNNFI9WkOpOpzAwhEEIcGyKc9HsgNhHgfn+UpNQ1lvLlZDtvdcklL0XoEh4pqRUr846nqLip0P2aNXnbSjj50jwmnNzM3TFZaAProdVBnZ1/j1mT6yYLxHCCNKMC/PDAAAAAElFTkSuQmCC",
- notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAdVBMVEUAAAAkSFwKChFAaX9JeZAABBhCbIEVAABIeI9yiJVObnxReI1MAABSeo9WcYBjfYw4WGsdKTiA0vd7y+5rS0233/JSHiPf8vnP4um85fnY6/LtAABZdobjAAB3xedyj55jiZ1Ba4BOTU9gREtAREvaAABiAADFYUIqAAAAEnRSTlMAIRHv3iDc3P3e3v7x7u7u7EYDRb2ZAAAAgklEQVQY05WPWQ7CMAxE67jpSlnsOk1Kyw73PyJJFCEQUqX6741H43G2MEqpH9blptRfK2jMbBoIDAUWoGvbH3tbg+ecibudHb0wjNvIRE7Ok/HCMGXIVGErzsl9jo4rER727UXk8bqFjOio+NSheYYrKYNzSD2iEjjV+fT4e2LFvAFuzge5hY5wYgAAAABJRU5ErkJggg==",
- }
- };
- let currentStyle = "default";
- let currentState = "base";
- let cachedUserStyle = null;
- function removeFavicons() {
- const head = document.head;
- if (!head) return;
- head.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]').forEach(link => link.remove());
- }
- function insertFavicon(href) {
- const head = document.head;
- if (!head) return;
- const link = document.createElement('link');
- link.rel = 'icon';
- link.type = 'image/png';
- link.href = href;
- head.appendChild(link);
- }
- async function getUserFaviconStyle() {
- if (cachedUserStyle) return cachedUserStyle;
- let style = "default";
- try {
- style = await getSetting("customFavicon_faviconStyle");
- } catch { }
- if (!STYLES.includes(style)) style = "default";
- cachedUserStyle = style;
- return style;
- }
- async function setFaviconStyle(style, state = "base") {
- if (!STYLES.includes(style)) style = "default";
- if (!STATES.includes(state)) state = "base";
- if (currentStyle === style && currentState === state) return;
- const url = (FAVICON_DATA?.[style]?.[state]) || FAVICON_DATA.default.base;
- removeFavicons();
- insertFavicon(url);
- currentStyle = style;
- currentState = state;
- document.dispatchEvent(new CustomEvent("faviconStateChanged", {
- detail: { style, state }
- }));
- }
- async function setFavicon(state = "base") {
- if (!STATES.includes(state)) state = "base";
- const style = await getUserFaviconStyle();
- await setFaviconStyle(style, state);
- }
- async function resetFavicon() {
- await setFavicon("base");
- }
- function getCurrentFaviconState() {
- return { style: currentStyle, state: currentState };
- }
- return {
- setFavicon,
- setFaviconStyle,
- resetFavicon,
- getCurrentFaviconState,
- STYLES,
- STATES
- };
- })();
- const featureMap = [
- { key: "enableScrollSave", fn: featureSaveScroll },
- { key: "watchThreadOnReply", fn: featureWatchThreadOnReply },
- { key: "blurSpoilers", fn: featureBlurSpoilers },
- { key: "enableHeaderCatalogLinks", fn: featureHeaderCatalogLinks },
- { key: "openCatalogThreadNewTab", fn: catalogThreadsInNewTab },
- { key: "enableScrollArrows", fn: featureScrollArrows },
- { key: "alwaysShowTW", fn: featureAlwaysShowTW },
- { key: "scrollToBottom", fn: preventFooterScrollIntoView },
- { key: "enableThreadHiding", fn: featureCatalogHiding },
- { key: "switchTimeFormat", fn: featureLabelCreated12h },
- { key: "enableIdFilters", fn: enableIdFiltering },
- { key: "enhanceYoutube", fn: enhanceYouTubeLinks },
- { key: "threadStatsInHeader", fn: threadInfoHeader },
- { key: "enableHashNav", fn: hashNavigation },
- { key: "hideAnnouncement", fn: featureHideAnnouncement },
- { key: "enableAutoHideHeaderScroll", fn: autoHideHeaderOnScroll },
- { key: "enableMediaViewer", fn: mediaViewerPositioning },
- { key: "customFavicon", fn: enableFavicon },
- { key: "highlightNewIds", fn: featureHighlightNewIds },
- { key: "quoteThreading", fn: featureQuoteThreading },
- { key: "enableLastFifty", fn: featureLastFifty },
- { key: "enableIdToggle", fn: featureToggleIdAsYours },
- { key: "enableTheSauce", fn: featureSauceLinks },
- { key: "enableUpdateNotif", fn: updateNotif },
- { key: "enableHidingMenu", fn: featureCustomPostHideMenu },
- { key: "alwaysShowIdCount", fn: featureShowIDCount },
- ];
- for (const { key, fn } of featureMap) {
- try {
- if (await getSetting(key)) {
- fn();
- }
- } catch (e) {
- console.error(`${fn.name || 'Feature'} failed:`, e);
- }
- }
- if (await getSetting("truncFilenames")) {
- try {
- const filenameLength = await getSetting("truncFilenames_customTrunc");
- truncateFilenames(filenameLength);
- } catch (e) {
- console.error("truncateFilenames failed:", e);
- }
- }
- async function enableFavicon() {
- try {
- const customFaviconEnabled = await getSetting("customFavicon");
- const selectedStyle = await getSetting("customFavicon_faviconStyle");
- if (customFaviconEnabled) {
- if (selectedStyle && typeof selectedStyle === 'string') {
- await faviconManager.setFaviconStyle(selectedStyle);
- } else {
- console.warn("Invalid favicon style:", selectedStyle);
- await faviconManager.setFaviconStyle("eight_dark");
- }
- } else {
- await faviconManager.resetFavicon();
- }
- } catch (e) {
- console.error("Error updating favicon:", e);
- }
- }
- let imageHoverEnabled = false;
- try {
- if (window.pageType?.isCatalog) {
- imageHoverEnabled = await getSetting("enableCatalogImageHover");
- } else {
- imageHoverEnabled = await getSetting("enableThreadImageHover");
- }
- if (imageHoverEnabled) {
- localStorage.removeItem("hoveringImage");
- featureImageHover();
- }
- } catch (e) {
- console.error("featureImageHover failed:", e);
- }
- (function () {
- function sanitizeToastHTML(html) {
- html = html.replace(/<(\/?)(?!a\b|b\b|i\b|u\b|strong\b|em\b)[^>]*>/gi, '');
- html = html.replace(/<(b|i|u|strong|em)[^>]*>/gi, '<$1>');
- html = html.replace(/<a\s+([^>]+)>/gi, function (match, attrs) {
- let allowed = '';
- attrs.replace(/(\w+)\s*=\s*(['"])(.*?)\2/gi, function (_, name, q, value) {
- name = name.toLowerCase();
- if (['href', 'target', 'rel'].includes(name)) {
- if (name === 'href' && (/^\s*javascript:/i.test(value) || /^\s*data:/i.test(value))) return;
- allowed += ` ${name}=${q}${value}${q}`;
- }
- });
- return `<a${allowed}>`;
- });
- return html;
- }
- const script = document.createElement('script');
- script.textContent = '(' + function (sanitizeToastHTML) {
- window.showGlobalToast = function (htmlMessage, color = "black", duration = 1200) {
- if (document.querySelector('.global-toast-notification')) {
- return;
- }
- const colorMap = {
- black: "#222",
- orange: "#cc7a00",
- green: "#339933",
- blue: "#1976d2",
- red: "#c62828"
- };
- const bgColor = colorMap[color] || color;
- const icon = document.getElementById("8chanSS-icon");
- let toast = document.createElement("span");
- toast.className = "global-toast-notification";
- toast.innerHTML = sanitizeToastHTML(htmlMessage);
- toast.style.position = "absolute";
- toast.style.background = bgColor;
- toast.style.color = "#fff";
- toast.style.padding = "2px 12px";
- toast.style.borderRadius = "4px";
- toast.style.fontSize = "13px";
- toast.style.zIndex = 99999;
- toast.style.opacity = "1";
- toast.style.transition = "opacity 0.3s";
- toast.style.pointerEvents = "auto";
- toast.style.boxShadow = "0 2px 8px rgba(0,0,0,0.18)";
- let closeBtn = document.createElement("span");
- closeBtn.textContent = "✕";
- closeBtn.style.marginLeft = "10px";
- closeBtn.style.cursor = "pointer";
- closeBtn.style.fontWeight = "bold";
- closeBtn.style.fontSize = "15px";
- closeBtn.style.opacity = "0.7";
- closeBtn.style.float = "right";
- closeBtn.style.userSelect = "none";
- closeBtn.onclick = function (e) {
- e.stopPropagation();
- if (toast.parentNode) toast.parentNode.removeChild(toast);
- if (timeout1) clearTimeout(timeout1);
- if (timeout2) clearTimeout(timeout2);
- };
- closeBtn.onmouseover = function () { closeBtn.style.opacity = "1"; };
- closeBtn.onmouseout = function () { closeBtn.style.opacity = "0.7"; };
- toast.appendChild(closeBtn);
- if (icon && icon.parentNode) {
- toast.style.left = (icon.offsetLeft - 50) + "px";
- toast.style.top = "28px";
- icon.parentNode.appendChild(toast);
- } else {
- toast.style.right = "25px";
- toast.style.top = "25px";
- toast.style.position = "fixed";
- document.body.appendChild(toast);
- }
- let timeout1 = setTimeout(() => { toast.style.opacity = "0"; }, duration - 300);
- let timeout2 = setTimeout(() => { toast.remove(); }, duration);
- };
- } + ')(' + sanitizeToastHTML.toString() + ');';
- document.documentElement.appendChild(script);
- script.remove();
- window.callPageToast = function (msg, color = 'black', duration = 1200) {
- const script = document.createElement('script');
- script.textContent = `window.showGlobalToast && window.showGlobalToast(${JSON.stringify(msg)}, ${JSON.stringify(color)}, ${duration});`;
- document.documentElement.appendChild(script);
- script.remove();
- };
- })();
- async function featureSaveScroll() {
- if (!window.pageType?.isThread) return;
- const STORAGE_KEY = "8chanSS_scrollPositions";
- const UNREAD_LINE_ID = "unread-line";
- const MAX_THREADS = 200;
- function getBoardAndThread() {
- const match = window.location.pathname.match(/^\/([^/]+)\/res\/([^/.]+)\.html$/i);
- if (!match) return null;
- return { board: match[1], thread: match[2] };
- }
- async function getAllSavedScrollData() {
- const saved = await GM.getValue(STORAGE_KEY, null);
- if (!saved) return {};
- try { return JSON.parse(saved); } catch { return {}; }
- }
- async function setAllSavedScrollData(data) {
- await GM.setValue(STORAGE_KEY, JSON.stringify(data));
- }
- function getCurrentPostCount() {
- if (!divPosts) return 0;
- return divPosts.querySelectorAll(":scope > .postCell[id]").length;
- }
- function removeUnreadLineMarker() {
- const oldMarker = document.getElementById(UNREAD_LINE_ID);
- if (oldMarker && oldMarker.parentNode) {
- oldMarker.parentNode.removeChild(oldMarker);
- }
- }
- let lastSeenPostCount = 0;
- let unseenCount = 0;
- let tabTitleBase = null;
- let previousFaviconState = null;
- const customFaviconEnabled = await getSetting("customFavicon");
- async function updateTabTitle() {
- if (window.isNotifying) return;
- if (!tabTitleBase) tabTitleBase = document.title.replace(/^\(\d+\)\s*/, "");
- document.title = unseenCount > 0 ? `(${unseenCount}) ${tabTitleBase}` : tabTitleBase;
- const { style, state } = faviconManager.getCurrentFaviconState();
- if (unseenCount > 0 && customFaviconEnabled) {
- if (state !== "unread") {
- previousFaviconState = { style, state };
- }
- faviconManager.setFaviconStyle(style, "unread");
- } else if (unseenCount == 0 && customFaviconEnabled) {
- if (state === "unread" && previousFaviconState) {
- faviconManager.setFaviconStyle(previousFaviconState.style, previousFaviconState.state);
- previousFaviconState = null;
- } else if (state === "unread") {
- faviconManager.setFavicon("base");
- }
- }
- }
- async function updateUnseenCountFromSaved() {
- const info = getBoardAndThread();
- if (!info) return;
- const allData = await getAllSavedScrollData();
- const key = `${info.board}/${info.thread}`;
- const saved = allData[key];
- const currentCount = getCurrentPostCount();
- lastSeenPostCount = (saved && typeof saved.lastSeenPostCount === "number") ? saved.lastSeenPostCount : 0;
- unseenCount = Math.max(0, currentCount - lastSeenPostCount);
- updateTabTitle();
- }
- let lastScrollY = window.scrollY;
- async function onScrollUpdateSeen() {
- const info = getBoardAndThread();
- if (!info || !(await getSetting("enableScrollSave"))) return;
- const posts = Array.from(document.querySelectorAll(".divPosts > .postCell[id]"));
- let maxIndex = -1;
- for (let i = 0; i < posts.length; ++i) {
- const rect = posts[i].getBoundingClientRect();
- if (rect.bottom > 0 && rect.top < window.innerHeight) maxIndex = i;
- }
- const currentCount = getCurrentPostCount();
- let newLastSeen = lastSeenPostCount;
- if (window.scrollY > lastScrollY) {
- if (maxIndex >= 0 && currentCount > 0) {
- if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 20)) {
- newLastSeen = currentCount;
- } else {
- newLastSeen = Math.max(lastSeenPostCount, maxIndex + 1);
- }
- }
- if (newLastSeen !== lastSeenPostCount) {
- lastSeenPostCount = newLastSeen;
- let allData = await getAllSavedScrollData();
- const key = `${info.board}/${info.thread}`;
- if (!allData[key]) allData[key] = {};
- allData[key].lastSeenPostCount = lastSeenPostCount;
- allData[key].timestamp = Date.now();
- if (
- typeof allData[key].position !== "number" ||
- window.scrollY > allData[key].position
- ) {
- allData[key].position = window.scrollY;
- }
- await setAllSavedScrollData(allData);
- }
- unseenCount = Math.max(0, currentCount - lastSeenPostCount);
- updateTabTitle();
- }
- lastScrollY = window.scrollY;
- }
- async function saveScrollPosition() {
- const info = getBoardAndThread();
- if (!info || !(await getSetting("enableScrollSave"))) return;
- const scrollPosition = window.scrollY;
- const timestamp = Date.now();
- let allData = await getAllSavedScrollData();
- const keys = Object.keys(allData);
- if (keys.length >= MAX_THREADS) {
- keys.sort((a, b) => (allData[a].timestamp || 0) - (allData[b].timestamp || 0));
- for (let i = 0; i < keys.length - MAX_THREADS + 1; ++i) delete allData[keys[i]];
- }
- const key = `${info.board}/${info.thread}`;
- if (!allData[key]) allData[key] = {};
- if (
- typeof allData[key].position !== "number" ||
- scrollPosition > allData[key].position
- ) {
- allData[key].position = scrollPosition;
- allData[key].timestamp = timestamp;
- await setAllSavedScrollData(allData);
- }
- }
- function scrollElementToViewportCenter(el) {
- if (!el) return;
- const rect = el.getBoundingClientRect();
- const elTop = rect.top + window.pageYOffset;
- const elHeight = rect.height;
- const viewportHeight = window.innerHeight;
- const scrollTo = elTop - (viewportHeight / 2) + (elHeight / 2);
- window.scrollTo({ top: scrollTo, behavior: "auto" });
- }
- async function restoreScrollPosition() {
- const info = getBoardAndThread();
- if (!info || !(await getSetting("enableScrollSave"))) return;
- const allData = await getAllSavedScrollData();
- const key = `${info.board}/${info.thread}`;
- const saved = allData[key];
- if (!saved || typeof saved.position !== "number") return;
- const anchor = window.location.hash ? window.location.hash.replace(/^#/, "") : null;
- const safeAnchor = anchor && /^[a-zA-Z0-9_-]+$/.test(anchor) ? anchor : null;
- if (safeAnchor) {
- setTimeout(() => {
- const post = document.getElementById(safeAnchor);
- if (post && post.classList.contains("postCell")) {
- scrollElementToViewportCenter(post);
- }
- addUnreadLineAtSavedScrollPosition(saved.position, false);
- }, 25);
- return;
- }
- saved.timestamp = Date.now();
- await setAllSavedScrollData(allData);
- window.scrollTo({ top: saved.position, behavior: "auto" });
- setTimeout(() => addUnreadLineAtSavedScrollPosition(saved.position, false), 80);
- }
- async function addUnreadLineAtSavedScrollPosition(scrollPosition, centerAfter = false) {
- if (!(await getSetting("enableScrollSave_showUnreadLine"))) return;
- if (!divPosts) return;
- const margin = 5;
- const docHeight = document.body.offsetHeight;
- if ((scrollPosition + window.innerHeight) >= (docHeight - margin)) {
- return;
- }
- const posts = Array.from(divPosts.querySelectorAll(":scope > .postCell[id]"));
- let targetPost = null;
- for (let i = 0; i < posts.length; ++i) {
- const postTop = posts[i].offsetTop;
- if (postTop > scrollPosition) break;
- targetPost = posts[i];
- }
- if (!targetPost) return;
- removeUnreadLineMarker();
- const marker = document.createElement("hr");
- marker.id = UNREAD_LINE_ID;
- if (targetPost.nextSibling) {
- divPosts.insertBefore(marker, targetPost.nextSibling);
- } else {
- divPosts.appendChild(marker);
- }
- if (centerAfter) {
- setTimeout(() => {
- const markerElem = document.getElementById(UNREAD_LINE_ID);
- if (markerElem) {
- const rect = markerElem.getBoundingClientRect();
- const desiredY = window.innerHeight / 3;
- const scrollY = window.scrollY + rect.top - desiredY;
- window.scrollTo({ top: scrollY, behavior: "auto" });
- }
- }, 25);
- }
- }
- let unseenUpdateTimeout = null;
- function debouncedUpdateUnseenCount() {
- if (unseenUpdateTimeout) clearTimeout(unseenUpdateTimeout);
- unseenUpdateTimeout = setTimeout(() => {
- updateUnseenCountFromSaved();
- unseenUpdateTimeout = null;
- }, 100);
- }
- const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: false });
- if (divPostsObs) {
- divPostsObs.addHandler(function saveScrollPostCountHandler() {
- debouncedUpdateUnseenCount();
- });
- }
- async function removeUnreadLineIfAtBottom() {
- if (!(await getSetting("enableScrollSave_showUnreadLine"))) return;
- const margin = 5;
- if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - margin)) {
- removeUnreadLineMarker();
- }
- }
- window.addEventListener("beforeunload", () => {
- saveScrollPosition();
- });
- document.addEventListener("DOMContentLoaded", () => {
- tabTitleBase = document.title.replace(/^\(\d+\)\s*/, "");
- updateTabTitle();
- });
- window.addEventListener("load", async () => {
- await restoreScrollPosition();
- await updateUnseenCountFromSaved();
- });
- let scrollTimeout = null;
- window.addEventListener("scroll", () => {
- if (scrollTimeout) return;
- scrollTimeout = setTimeout(async () => {
- await onScrollUpdateSeen();
- await removeUnreadLineIfAtBottom();
- scrollTimeout = null;
- }, 100);
- });
- await restoreScrollPosition();
- await updateUnseenCountFromSaved();
- }
- async function featureHeaderCatalogLinks() {
- async function appendCatalogToLinks() {
- const navboardsSpan = document.getElementById("navBoardsSpan");
- if (navboardsSpan) {
- const links = navboardsSpan.getElementsByTagName("a");
- const openInNewTab = await getSetting("enableHeaderCatalogLinks_openInNewTab");
- for (let link of links) {
- if (
- link.href &&
- !link.href.endsWith("/catalog.html") &&
- !link.dataset.catalogLinkProcessed
- ) {
- link.href += "/catalog.html";
- link.dataset.catalogLinkProcessed = "1";
- if (openInNewTab) {
- link.target = "_blank";
- link.rel = "noopener noreferrer";
- } else {
- link.target = "";
- link.rel = "";
- }
- }
- }
- }
- }
- appendCatalogToLinks();
- const debouncedAppend = debounce(appendCatalogToLinks, 100);
- const navboardsObs = observeSelector('#navBoardsSpan', { childList: true, subtree: true });
- if (navboardsObs) {
- navboardsObs.addHandler(function headerCatalogLinksHandler() {
- debouncedAppend();
- });
- }
- }
- function catalogThreadsInNewTab() {
- if (!window.pageType?.isCatalog) return;
- catalogDiv.querySelectorAll('.catalogCell a.linkThumb').forEach(link => {
- if (link.getAttribute('target') !== '_blank') {
- link.setAttribute('target', '_blank');
- }
- });
- catalogDiv.addEventListener('click', function (e) {
- const link = e.target.closest('.catalogCell a.linkThumb');
- if (link && link.getAttribute('target') !== '_blank') {
- link.setAttribute('target', '_blank');
- }
- });
- }
- function featureImageHover() {
- const MEDIA_MAX_WIDTH = "90vw";
- const MEDIA_OPACITY_LOADING = "0";
- const MEDIA_OPACITY_LOADED = "1";
- const MEDIA_OFFSET = 50;
- const MEDIA_BOTTOM_MARGIN = 3;
- const AUDIO_INDICATOR_TEXT = "▶ Playing audio...";
- function getExtensionForMimeType(mime) {
- const map = {
- "image/jpeg": ".jpg",
- "image/jpg": ".jpg",
- "image/jxl": ".jxl",
- "image/png": ".png",
- "image/apng": ".png",
- "image/gif": ".gif",
- "image/avif": ".avif",
- "image/webp": ".webp",
- "image/bmp": ".bmp",
- "video/mp4": ".mp4",
- "video/webm": ".webm",
- "video/x-m4v": ".m4v",
- "audio/ogg": ".ogg",
- "audio/mpeg": ".mp3",
- "audio/x-m4a": ".m4a",
- "audio/x-wav": ".wav",
- };
- return map[mime.toLowerCase()] || null;
- }
- function sanitizeUrl(url) {
- try {
- const parsed = new URL(url, window.location.origin);
- if ((parsed.protocol === "http:" || parsed.protocol === "https:") &&
- parsed.origin === window.location.origin) {
- return parsed.href;
- }
- } catch { }
- return "";
- }
- let floatingMedia = null;
- let cleanupFns = [];
- let currentAudioIndicator = null;
- let lastMouseEvent = null;
- function clamp(val, min, max) {
- return Math.max(min, Math.min(max, val));
- }
- function positionFloatingMedia(event) {
- if (!floatingMedia) return;
- const vw = window.innerWidth;
- const vh = window.innerHeight;
- const mw = floatingMedia.offsetWidth || 0;
- const mh = floatingMedia.offsetHeight || 0;
- const docElement = document.documentElement;
- const SCROLLBAR_WIDTH = window.innerWidth - docElement.clientWidth;
- const MEDIA_BOTTOM_MARGIN_PX = vh * (MEDIA_BOTTOM_MARGIN / 100);
- let x, y;
- const rightX = event.clientX + MEDIA_OFFSET;
- const leftX = event.clientX - MEDIA_OFFSET - mw;
- if (rightX + mw <= vw - SCROLLBAR_WIDTH) {
- x = rightX;
- }
- else if (leftX >= 0) {
- x = leftX;
- }
- else {
- x = clamp(rightX, 0, vw - mw - SCROLLBAR_WIDTH);
- }
- y = event.clientY;
- const maxY = vh - mh - MEDIA_BOTTOM_MARGIN_PX;
- y = Math.max(0, Math.min(y, maxY));
- floatingMedia.style.left = `${x}px`;
- floatingMedia.style.top = `${y}px`;
- }
- function cleanupFloatingMedia() {
- cleanupFns.forEach(fn => { try { fn(); } catch { } });
- cleanupFns = [];
- if (floatingMedia) {
- if (["VIDEO", "AUDIO"].includes(floatingMedia.tagName)) {
- try {
- floatingMedia.pause();
- floatingMedia.srcObject = null;
- URL.revokeObjectURL(floatingMedia.src);
- floatingMedia.removeAttribute("src");
- floatingMedia.load();
- } catch { }
- }
- floatingMedia.remove();
- floatingMedia = null;
- }
- if (currentAudioIndicator && currentAudioIndicator.parentNode) {
- currentAudioIndicator.parentNode.removeChild(currentAudioIndicator);
- currentAudioIndicator = null;
- }
- }
- function getFullMediaSrc(thumbNode, filemime) {
- const thumbnailSrc = thumbNode.getAttribute("src");
- const parentA = thumbNode.closest("a.linkThumb, a.imgLink");
- const href = parentA ? parentA.getAttribute("href") : "";
- const fileWidth = parentA ? parseInt(parentA.getAttribute("data-filewidth"), 10) : null;
- const fileHeight = parentA ? parseInt(parentA.getAttribute("data-fileheight"), 10) : null;
- function hasExtension(str) {
- return /\.[a-z0-9]+$/i.test(str);
- }
- function isTThumb(str) {
- return /\/t_/.test(str);
- }
- function isDirectHash(str) {
- return /^\/\.media\/[a-f0-9]{40,}$/i.test(str) && !hasExtension(str);
- }
- function isSmallImage() {
- return (fileWidth && fileWidth <= 220) || (fileHeight && fileHeight <= 220);
- }
- function isBarePngNoThumb() {
- return (
- filemime &&
- filemime.toLowerCase() === "image/png" &&
- parentA &&
- !isTThumb(href) &&
- !hasExtension(href)
- );
- }
- function isSmallBarePngSrc() {
- return (
- isSmallImage() &&
- filemime &&
- filemime.toLowerCase() === "image/png" &&
- !isTThumb(thumbnailSrc) &&
- !hasExtension(thumbnailSrc)
- );
- }
- function isGenericThumb() {
- return (
- /\/spoiler\.png$/i.test(thumbnailSrc) ||
- /\/custom\.spoiler$/i.test(thumbnailSrc) ||
- /\/audioGenericThumb\.png$/i.test(thumbnailSrc)
- );
- }
- if (!filemime) {
- if (
- thumbNode.closest('.catalogCell') ||
- /^\/\.media\/t?_[a-f0-9]{40,}$/i.test(thumbnailSrc.replace(/\\/g, ''))
- ) {
- return thumbnailSrc;
- }
- return null;
- }
- if (isBarePngNoThumb()) {
- return thumbnailSrc;
- }
- if (isSmallBarePngSrc()) {
- return thumbnailSrc;
- }
- if (isSmallImage() && hasExtension(thumbnailSrc)) {
- return thumbnailSrc;
- }
- if (isTThumb(thumbnailSrc)) {
- let base = thumbnailSrc.replace(/\/t_/, "/");
- base = base.replace(/\.(jpe?g|jxl|png|apng|gif|avif|webp|webm|mp4|m4v|ogg|mp3|m4a|wav)$/i, "");
- if (filemime && (filemime.toLowerCase() === "image/apng" || filemime.toLowerCase() === "video/x-m4v")) {
- return base;
- }
- const ext = filemime ? getExtensionForMimeType(filemime) : null;
- if (!ext) return null;
- return base + ext;
- }
- if (isDirectHash(thumbnailSrc)) {
- if (filemime && (filemime.toLowerCase() === "image/apng" || filemime.toLowerCase() === "video/x-m4v")) {
- return thumbnailSrc;
- }
- const ext = filemime ? getExtensionForMimeType(filemime) : null;
- if (!ext) {
- return thumbnailSrc;
- }
- return thumbnailSrc + ext;
- }
- if (isGenericThumb()) {
- if (parentA && parentA.getAttribute("href")) {
- return sanitizeUrl(parentA.getAttribute("href"));
- }
- return null;
- }
- return null;
- }
- function leaveHandler() {
- cleanupFloatingMedia();
- }
- function mouseMoveHandler(ev) {
- lastMouseEvent = ev;
- positionFloatingMedia(ev);
- }
- async function onThumbEnter(e) {
- cleanupFloatingMedia();
- lastMouseEvent = e;
- const thumb = e.currentTarget;
- let filemime = null, fullSrc = null, isVideo = false, isAudio = false;
- if (thumb.tagName === "IMG") {
- const parentA = thumb.closest("a.linkThumb, a.imgLink");
- if (!parentA) return;
- const href = parentA.getAttribute("href");
- if (!href) return;
- const ext = href.split(".").pop().toLowerCase();
- filemime =
- parentA.getAttribute("data-filemime") ||
- {
- jpg: "image/jpeg",
- jpeg: "image/jpeg",
- jxl: "image/jxl",
- png: "image/png",
- apng: "image/apng",
- gif: "image/gif",
- avif: "image/avif",
- webp: "image/webp",
- bmp: "image/bmp",
- mp4: "video/mp4",
- webm: "video/webm",
- m4v: "video/x-m4v",
- ogg: "audio/ogg",
- mp3: "audio/mpeg",
- m4a: "audio/x-m4a",
- wav: "audio/wav",
- }[ext];
- fullSrc = getFullMediaSrc(thumb, filemime);
- isVideo = filemime && filemime.startsWith("video/");
- isAudio = filemime && filemime.startsWith("audio/");
- }
- fullSrc = sanitizeUrl(fullSrc);
- if (!fullSrc) return;
- let volume = 0.5;
- try {
- if (typeof getSetting === "function") {
- const v = await getSetting("hoverVideoVolume");
- if (typeof v === "number" && !isNaN(v)) {
- volume = Math.max(0, Math.min(1, v / 100));
- }
- }
- } catch { }
- if (isAudio) {
- const container = thumb.closest("a.linkThumb, a.imgLink");
- if (container && !container.style.position) {
- container.style.position = "relative";
- }
- floatingMedia = document.createElement("audio");
- floatingMedia.src = fullSrc;
- floatingMedia.controls = false;
- floatingMedia.style.display = "none";
- floatingMedia.volume = volume;
- document.body.appendChild(floatingMedia);
- floatingMedia.play().catch(() => { });
- const indicator = document.createElement("div");
- indicator.classList.add("audio-preview-indicator");
- indicator.textContent = AUDIO_INDICATOR_TEXT;
- if (container) {
- container.appendChild(indicator);
- }
- currentAudioIndicator = indicator;
- thumb.addEventListener("mouseleave", leaveHandler, { once: true });
- if (container) container.addEventListener("click", leaveHandler, { once: true });
- window.addEventListener("scroll", leaveHandler, { passive: true, once: true });
- cleanupFns.push(() => thumb.removeEventListener("mouseleave", leaveHandler));
- if (container) cleanupFns.push(() => container.removeEventListener("click", leaveHandler));
- cleanupFns.push(() => window.removeEventListener("scroll", leaveHandler));
- return;
- }
- floatingMedia = isVideo ? document.createElement("video") : document.createElement("img");
- floatingMedia.src = fullSrc;
- floatingMedia.id = "hover-preview-media";
- floatingMedia.style.position = "fixed";
- floatingMedia.style.zIndex = "9999";
- floatingMedia.style.pointerEvents = "none";
- floatingMedia.style.opacity = MEDIA_OPACITY_LOADING;
- floatingMedia.style.left = "-9999px";
- floatingMedia.style.top = "-9999px";
- floatingMedia.style.maxWidth = MEDIA_MAX_WIDTH;
- const availableHeight = window.innerHeight * (1 - MEDIA_BOTTOM_MARGIN / 100);
- floatingMedia.style.maxHeight = `${availableHeight}px`;
- if (isVideo) {
- floatingMedia.autoplay = true;
- floatingMedia.loop = true;
- floatingMedia.muted = false;
- floatingMedia.playsInline = true;
- floatingMedia.volume = volume;
- }
- document.body.appendChild(floatingMedia);
- document.addEventListener("mousemove", mouseMoveHandler, { passive: true });
- thumb.addEventListener("mouseleave", leaveHandler, { passive: true, once: true });
- cleanupFns.push(() => document.removeEventListener("mousemove", mouseMoveHandler));
- if (lastMouseEvent) {
- positionFloatingMedia(lastMouseEvent);
- }
- if (isVideo) {
- floatingMedia.onloadeddata = function () {
- if (floatingMedia) {
- floatingMedia.style.opacity = MEDIA_OPACITY_LOADED;
- if (lastMouseEvent) positionFloatingMedia(lastMouseEvent);
- }
- };
- } else {
- floatingMedia.onload = function () {
- if (floatingMedia) {
- floatingMedia.style.opacity = MEDIA_OPACITY_LOADED;
- if (lastMouseEvent) positionFloatingMedia(lastMouseEvent);
- }
- };
- }
- floatingMedia.onerror = cleanupFloatingMedia;
- thumb.addEventListener("mouseleave", leaveHandler, { once: true });
- window.addEventListener("scroll", leaveHandler, { passive: true, once: true });
- cleanupFns.push(() => thumb.removeEventListener("mouseleave", leaveHandler));
- cleanupFns.push(() => window.removeEventListener("scroll", leaveHandler));
- }
- function attachThumbListeners(root = document) {
- root.querySelectorAll("a.linkThumb > img, a.imgLink > img").forEach(thumb => {
- if (!thumb._fullImgHoverBound) {
- thumb.addEventListener("mouseenter", onThumbEnter);
- thumb._fullImgHoverBound = true;
- }
- });
- if (
- root.tagName === "IMG" &&
- root.parentElement &&
- (root.parentElement.matches("a.linkThumb") || root.parentElement.matches("a.imgLink")) &&
- !root._fullImgHoverBound
- ) {
- root.addEventListener("mouseenter", onThumbEnter);
- root._fullImgHoverBound = true;
- }
- }
- attachThumbListeners();
- const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
- if (divThreadsObs) {
- divThreadsObs.addHandler(function imageHoverHandler(mutations, node) {
- for (const mutation of mutations) {
- for (const addedNode of mutation.addedNodes) {
- if (addedNode.nodeType === 1) {
- attachThumbListeners(addedNode);
- }
- }
- }
- });
- }
- }
- function getExtensionForMimeType(mime) {
- const map = {
- "image/jpeg": ".jpg",
- "image/jpg": ".jpg",
- "image/jxl": ".jxl",
- "image/png": ".png",
- "image/apng": ".png",
- "image/gif": ".gif",
- "image/avif": ".avif",
- "image/webp": ".webp",
- "image/bmp": ".bmp",
- };
- return map[mime.toLowerCase()] || "";
- }
- async function featureBlurSpoilers() {
- if (!(window.pageType?.isThread || window.pageType?.isIndex)) {
- return;
- }
- const removeSpoilers = await getSetting("blurSpoilers_removeSpoilers");
- function applyBlurOrRemoveSpoilers(img, removeSpoilers) {
- if (removeSpoilers) {
- img.style.filter = "";
- img.style.transition = "";
- img.style.border = "1px dotted var(--border-color)";
- img.onmouseover = null;
- img.onmouseout = null;
- } else {
- img.style.filter = "blur(5px)";
- img.style.transition = "filter 0.3s ease";
- }
- }
- function processImgLink(link) {
- if (link.dataset.blurSpoilerProcessed === "1") {
- return;
- }
- const img = link.querySelector("img");
- if (!img) {
- return;
- }
- if (
- /\/\.media\/[^\/]+?\.[a-zA-Z0-9]+$/.test(img.src) &&
- !/\/\.media\/t_[^\/]+?\.[a-zA-Z0-9]+$/.test(img.src)
- ) {
- link.dataset.blurSpoilerProcessed = "1";
- return;
- }
- const isCustomSpoiler = img.src.includes("/custom.spoiler")
- || img.src.includes("/*/custom.spoiler")
- || img.src.includes("/spoiler.png");
- const isNotThumbnail = !img.src.includes("/.media/t_");
- const hasFilenameExtension = !isCustomSpoiler && /\.[a-zA-Z0-9]+$/.test(img.src);
- if (isNotThumbnail || isCustomSpoiler) {
- let href = link.getAttribute("href");
- if (!href) {
- link.dataset.blurSpoilerProcessed = "1";
- return;
- }
- const match = href.match(/\/\.media\/([^\/]+)\.[a-zA-Z0-9]+$/);
- if (!match) {
- link.dataset.blurSpoilerProcessed = "1";
- return;
- }
- const fileMime = link.getAttribute("data-filemime") || "";
- const ext = getExtensionForMimeType(fileMime);
- let fileWidthAttr = link.getAttribute("data-filewidth");
- let fileHeightAttr = link.getAttribute("data-fileheight");
- let transformedSrc;
- if (
- (fileWidthAttr && Number(fileWidthAttr) <= 220) ||
- (fileHeightAttr && Number(fileHeightAttr) <= 220)
- ) {
- transformedSrc = `/.media/${match[1]}${ext}`;
- } else if (!hasFilenameExtension && isCustomSpoiler) {
- transformedSrc = `/.media/t_${match[1]}`;
- } else {
- link.dataset.blurSpoilerProcessed = "1";
- return;
- }
- if (isCustomSpoiler && !fileWidthAttr && !fileHeightAttr) {
- const uploadCell = img.closest('.uploadCell');
- if (uploadCell) {
- const dimensionLabel = uploadCell.querySelector('.dimensionLabel');
- if (dimensionLabel) {
- const dimensions = dimensionLabel.textContent.trim().split(/x|×/);
- if (dimensions.length === 2) {
- const parsedWidth = parseInt(dimensions[0].trim(), 10);
- const parsedHeight = parseInt(dimensions[1].trim(), 10);
- if ((parsedWidth <= 220 || parsedHeight <= 220)) {
- img.src = href;
- link.dataset.blurSpoilerProcessed = "1";
- applyBlurOrRemoveSpoilers(img, removeSpoilers);
- return;
- }
- }
- }
- }
- }
- const initialWidth = img.offsetWidth;
- const initialHeight = img.offsetHeight;
- img.style.width = initialWidth + "px";
- img.style.height = initialHeight + "px";
- img.src = transformedSrc;
- img.addEventListener('load', function () {
- img.style.width = img.naturalWidth + "px";
- img.style.height = img.naturalHeight + "px";
- });
- applyBlurOrRemoveSpoilers(img, removeSpoilers);
- link.dataset.blurSpoilerProcessed = "1";
- return;
- }
- link.dataset.blurSpoilerProcessed = "1";
- }
- document.querySelectorAll("a.imgLink").forEach(link => processImgLink(link));
- let pendingImgLinks = new WeakSet();
- let debounceTimeout = null;
- function processPendingImgLinks() {
- const linksToProcess = Array.from(document.querySelectorAll("a.imgLink")).filter(link => pendingImgLinks.has(link));
- linksToProcess.forEach(link => processImgLink(link));
- pendingImgLinks = new WeakSet();
- debounceTimeout = null;
- }
- const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
- if (divThreadsObs) {
- divThreadsObs.addHandler(function blurSpoilersHandler(mutations) {
- for (const mutation of mutations) {
- for (const addedNode of mutation.addedNodes) {
- if (addedNode.nodeType !== 1) continue;
- if (addedNode.classList && addedNode.classList.contains('imgLink')) {
- pendingImgLinks.add(addedNode);
- } else if (addedNode.querySelectorAll) {
- addedNode.querySelectorAll('.imgLink').forEach(link => pendingImgLinks.add(link));
- }
- }
- }
- if (!debounceTimeout) {
- debounceTimeout = setTimeout(processPendingImgLinks, 50);
- }
- });
- }
- const bodyObs = observeSelector('body', { childList: true, subtree: true });
- if (bodyObs) {
- bodyObs.addHandler(function quoteTooltipSpoilerHandler(mutations) {
- for (const mutation of mutations) {
- for (const addedNode of mutation.addedNodes) {
- if (addedNode.nodeType !== 1) continue;
- if (addedNode.classList && addedNode.classList.contains('quoteTooltip')) {
- addedNode.querySelectorAll('a.imgLink').forEach(link => processImgLink(link));
- } else if (addedNode.querySelectorAll) {
- addedNode.querySelectorAll('.quoteTooltip a.imgLink').forEach(link => processImgLink(link));
- }
- }
- }
- });
- }
- document.body.addEventListener("mouseover", function (e) {
- if (e.target.matches("a.imgLink img[style*='blur(5px)']")) {
- e.target.style.filter = "none";
- }
- });
- document.body.addEventListener("mouseout", function (e) {
- if (e.target.matches("a.imgLink img[style*='transition']")) {
- e.target.style.filter = "blur(5px)";
- }
- });
- }
- function autoHideHeaderOnScroll() {
- const header = document.getElementById('dynamicHeaderThread');
- if (!header) return;
- const scrollThreshold = 50;
- let lastScrollY = window.scrollY;
- let scrollDirection = 'none';
- let ticking = false;
- function updateHeaderVisibility() {
- const currentScrollY = window.scrollY;
- scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up';
- lastScrollY = currentScrollY;
- const isNearTop = currentScrollY < 100;
- if (scrollDirection === 'up' || isNearTop) {
- header.classList.remove('nav-hidden');
- } else if (scrollDirection === 'down' && currentScrollY > scrollThreshold) {
- header.classList.add('nav-hidden');
- }
- ticking = false;
- }
- const style = document.createElement('style');
- style.textContent = `
- #dynamicHeaderThread {
- transition: transform 0.3s ease;
- }
- #dynamicHeaderThread.nav-hidden {
- transform: translateY(-100%);
- }
- :root.bottom-header #dynamicHeaderThread.nav-hidden {
- transform: translateY(100%);
- }
- `;
- document.head.appendChild(style);
- window.addEventListener('scroll', () => {
- if (!ticking) {
- window.requestAnimationFrame(updateHeaderVisibility);
- ticking = true;
- }
- }, { passive: true });
- updateHeaderVisibility();
- }
- const decodeHtmlEntitiesTwice = (() => {
- const txt = document.createElement('textarea');
- return function (html) {
- txt.innerHTML = html;
- const once = txt.value;
- txt.innerHTML = once;
- return txt.value;
- };
- })();
- function highlightMentions() {
- const watchedCells = document.querySelectorAll("#watchedMenu .watchedCell");
- const watchButton = document.querySelector(".opHead .watchButton");
- if (!watchedCells.length) return;
- watchedCells.forEach((cell) => {
- const notification = cell.querySelector(".watchedCellLabel span.watchedNotification");
- if (!notification) return;
- const labelLink = cell.querySelector(".watchedCellLabel a");
- if (!labelLink) return;
- if (!labelLink.dataset.board) {
- const href = labelLink.getAttribute("href");
- const match = href?.match(/^(?:https?:\/\/[^\/]+)?\/([^\/]+)\//);
- if (match) {
- labelLink.dataset.board = `/${match[1]}/ -`;
- }
- if (document.location.href.includes(href)) {
- if (watchButton) {
- watchButton.style.color = "var(--board-title-color)";
- watchButton.title = "Watched";
- }
- }
- const originalText = labelLink.textContent;
- const decodedText = decodeHtmlEntitiesTwice(originalText);
- if (labelLink.textContent !== decodedText) {
- labelLink.textContent = decodedText;
- }
- }
- const notificationText = notification.textContent.trim();
- function styleMentionYou(labelLink, notification, totalReplies) {
- labelLink.style.color = "var(--board-title-color)";
- notification.style.color = "var(--board-title-color)";
- notification.textContent = ` (${totalReplies}) (You)`;
- notification.style.fontWeight = "bold";
- }
- function styleMentionNumber(notification, notificationText) {
- notification.textContent = ` (${notificationText})`;
- notification.style.color = "var(--link-color)";
- notification.style.fontWeight = "bold";
- }
- if (notificationText.startsWith("(") === true) {
- return;
- }
- if (notificationText.includes("(you)") === true) {
- const parts = notificationText.split(", ");
- const totalReplies = parts[0];
- styleMentionYou(labelLink, notification, totalReplies);
- }
- else if (/^\d+$/.test(notificationText)) {
- styleMentionNumber(notification, notificationText);
- }
- notification.dataset.processed = "true";
- });
- }
- const watchedMenuObs = observeSelector('#watchedMenu', { childList: true, subtree: true });
- if (watchedMenuObs) {
- watchedMenuObs.addHandler(function highlightMentionsHandler() {
- highlightMentions();
- });
- }
- highlightMentions();
- async function featureWatchThreadOnReply() {
- if ((window.pageType?.isIndex || window.pageType?.isCatalog)) {
- return;
- }
- const getWatchButton = () => document.querySelector(".watchButton");
- function watchThreadIfNotWatched() {
- const btn = getWatchButton();
- if (btn && !btn.classList.contains("watched-active")) {
- btn.click();
- setTimeout(() => {
- btn.classList.add("watched-active");
- }, 100);
- }
- }
- function updateWatchButtonClass() {
- const btn = getWatchButton();
- if (!btn) return;
- if (btn.classList.contains("watched-active")) {
- btn.classList.add("watched-active");
- } else {
- btn.classList.remove("watched-active");
- }
- }
- const submitButton = document.getElementById("qrbutton");
- if (submitButton) {
- submitButton.removeEventListener("click", submitButton._watchThreadHandler || (() => { }));
- submitButton._watchThreadHandler = async function () {
- if (await getSetting("watchThreadOnReply")) {
- setTimeout(watchThreadIfNotWatched, 500);
- }
- };
- submitButton.addEventListener("click", submitButton._watchThreadHandler);
- }
- updateWatchButtonClass();
- const btn = getWatchButton();
- if (btn) {
- btn.removeEventListener("click", btn._updateWatchHandler || (() => { }));
- btn._updateWatchHandler = () => setTimeout(updateWatchButtonClass, 100);
- btn.addEventListener("click", btn._updateWatchHandler);
- }
- }
- async function featureAlwaysShowTW() {
- if (!(await getSetting("alwaysShowTW"))) return;
- if ((await getSetting("alwaysShowTW_noPinInCatalog")) && window.pageType.isCatalog) return;
- function showThreadWatcher() {
- const watchedMenu = document.getElementById("watchedMenu");
- if (watchedMenu) {
- watchedMenu.style.display = "flex";
- }
- }
- showThreadWatcher();
- }
- (function markAllThreadsAsRead() {
- const handleDiv = document.querySelector('#watchedMenu > div.handle');
- if (!handleDiv) return;
- if (handleDiv.querySelector('.watchedCellDismissButton.markAllRead')) return;
- const btn = document.createElement('a');
- btn.className = 'watchedCellDismissButton glowOnHover coloredIcon markAllRead';
- btn.title = 'Mark all threads as read';
- btn.style.float = 'right';
- btn.style.paddingTop = '3px';
- function hasUnreadThreads() {
- const watchedMenu = document.querySelector('#watchedMenu > div.floatingContainer');
- if (!watchedMenu) return false;
- return watchedMenu.querySelectorAll('td.watchedCellDismissButton.glowOnHover.coloredIcon[title="Mark as read"]').length > 0;
- }
- function updateButtonState() {
- if (hasUnreadThreads()) {
- btn.style.opacity = '1';
- btn.style.pointerEvents = 'auto';
- btn.title = 'Mark all threads as read';
- } else {
- btn.style.opacity = '0.5';
- btn.style.pointerEvents = 'none';
- btn.title = 'No unread threads';
- }
- }
- function clickAllMarkAsReadButtons(watchedMenu) {
- const markButtons = watchedMenu.querySelectorAll('td.watchedCellDismissButton.glowOnHover.coloredIcon[title="Mark as read"]');
- markButtons.forEach(btn => {
- try {
- btn.click();
- } catch (e) {
- console.log("Error clicking button:", e);
- }
- });
- return markButtons.length;
- }
- function markAllThreadsAsReadWithRetry(retriesLeft, callback) {
- setTimeout(function () {
- const watchedMenu = document.querySelector('#watchedMenu > div.floatingContainer');
- if (!watchedMenu) {
- if (callback) callback();
- return;
- }
- const clickedCount = clickAllMarkAsReadButtons(watchedMenu);
- if (clickedCount === 0) {
- updateButtonState();
- if (callback) callback();
- return;
- }
- if (retriesLeft > 0) {
- setTimeout(() => markAllThreadsAsReadWithRetry(retriesLeft - 1, callback), 200);
- } else if (callback) {
- callback();
- }
- }, 100);
- }
- const watchedMenuObs = observeSelector('#watchedMenu > div.floatingContainer', { childList: true, subtree: true });
- if (watchedMenuObs) {
- const debouncedUpdate = debounce(updateButtonState, 100);
- watchedMenuObs.addHandler(function markAllThreadsAsReadHandler() {
- debouncedUpdate();
- });
- }
- updateButtonState();
- handleDiv.appendChild(btn);
- document.body.addEventListener('click', function (e) {
- const closeBtn = e.target.closest('#watchedMenu .close-btn');
- if (closeBtn) {
- const watchedMenu = document.getElementById("watchedMenu");
- if (watchedMenu) watchedMenu.style.display = "none";
- return;
- }
- const markAllBtn = e.target.closest('.watchedCellDismissButton.markAllRead');
- if (markAllBtn) {
- e.preventDefault();
- if (markAllBtn.style.pointerEvents === 'none' || markAllBtn.dataset.processing === 'true') return;
- markAllBtn.dataset.processing = 'true';
- markAllBtn.style.opacity = '0.5';
- markAllThreadsAsReadWithRetry(3, function () {
- markAllBtn.dataset.processing = 'false';
- updateButtonState();
- });
- }
- });
- })();
- function hashNavigation() {
- if (!window.pageType?.isThread) return;
- const processedLinks = new WeakSet();
- function addHashLinks(container = document) {
- const links = container.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
- links.forEach(link => {
- if (
- processedLinks.has(link) ||
- (link.nextSibling && link.nextSibling.classList && link.nextSibling.classList.contains('hash-link-container'))
- ) return;
- const hashSpan = document.createElement('span');
- hashSpan.textContent = ' #';
- hashSpan.className = 'hash-link';
- hashSpan.style.cursor = 'pointer';
- hashSpan.style.color = 'var(--navbar-text-color)';
- hashSpan.title = 'Scroll to post';
- const wrapper = document.createElement('span');
- wrapper.className = 'hash-link-container';
- wrapper.appendChild(hashSpan);
- link.insertAdjacentElement('afterend', wrapper);
- processedLinks.add(link);
- });
- }
- addHashLinks();
- const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
- if (divThreadsObs) {
- const debouncedAddHashLinks = debounce(() => addHashLinks(), 25);
- divThreadsObs.addHandler(function hashNavigationHandler() {
- debouncedAddHashLinks();
- });
- }
- const postsContainer = document.getElementById('divThreads') || document.body;
- postsContainer.addEventListener('click', function (e) {
- if (e.target.classList.contains('hash-link')) {
- e.preventDefault();
- const link = e.target.closest('.hash-link-container').previousElementSibling;
- if (!link || !link.href) return;
- const hashMatch = link.href.match(/#(\d+)$/);
- if (!hashMatch) return;
- const postId = hashMatch[1];
- const safePostId = /^[0-9]+$/.test(postId) ? postId : null;
- if (!safePostId) return;
- const postElem = document.getElementById(safePostId);
- if (postElem) {
- window.location.hash = `#${safePostId}`;
- if (postElem.classList.contains('opCell')) {
- const offset = 25;
- const rect = postElem.getBoundingClientRect();
- const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
- const targetY = rect.top + scrollTop - offset;
- window.scrollTo({ top: targetY, behavior: "smooth" });
- } else {
- postElem.scrollIntoView({ behavior: "smooth", block: "center" });
- }
- }
- }
- }, true);
- }
- function featureScrollArrows() {
- if (document.getElementById("scroll-arrow-up") || document.getElementById("scroll-arrow-down")) {
- return;
- }
- const upBtn = document.createElement("button");
- upBtn.id = "scroll-arrow-up";
- upBtn.className = "scroll-arrow-btn";
- upBtn.title = "Scroll to top";
- upBtn.innerHTML = "▲";
- upBtn.addEventListener("click", () => {
- window.scrollTo({ top: 0, behavior: "smooth" });
- });
- const downBtn = document.createElement("button");
- downBtn.id = "scroll-arrow-down";
- downBtn.className = "scroll-arrow-btn";
- downBtn.title = "Scroll to bottom";
- downBtn.innerHTML = "▼";
- downBtn.addEventListener("click", () => {
- window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
- });
- document.body.appendChild(upBtn);
- document.body.appendChild(downBtn);
- }
- async function featureHideAnnouncement() {
- async function processElement(selector, settingKey, contentKey) {
- const el = document.querySelector(selector);
- if (!el) return;
- const content = (el.textContent || "").replace(/[^\w\s.,!?-]/g, "");
- const shouldHide = await GM.getValue(`8chanSS_${settingKey}`, "false") === "true";
- const storedContent = await GM.getValue(`8chanSS_${contentKey}`, null);
- const root = document.documentElement;
- if (shouldHide) {
- if (storedContent !== null && storedContent !== content) {
- if (typeof window.setSetting === "function") {
- await window.setSetting("hideAnnouncement", false);
- }
- await GM.setValue(`8chanSS_${settingKey}`, "false");
- await GM.deleteValue(`8chanSS_${contentKey}`);
- return;
- }
- root.classList.add("hide-announcement");
- await GM.setValue(`8chanSS_${contentKey}`, content);
- } else {
- root.classList.remove("hide-announcement");
- await GM.deleteValue(`8chanSS_${contentKey}`);
- }
- }
- await processElement("#dynamicAnnouncement", "hideAnnouncement", "announcementContent");
- }
- (async function featureBeepOnYou() {
- if (!divPosts) return;
- let audioContext = null;
- let audioContextReady = false;
- let audioContextPromise = null;
- function ensureAudioContextReady() {
- if (audioContextReady) return Promise.resolve();
- if (audioContextPromise) return audioContextPromise;
- audioContextPromise = new Promise((resolve) => {
- function resumeAudioContext() {
- if (!audioContext) {
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
- }
- if (audioContext.state === 'suspended') {
- audioContext.resume().then(() => {
- audioContextReady = true;
- window.removeEventListener('click', resumeAudioContext);
- window.removeEventListener('keydown', resumeAudioContext);
- resolve();
- });
- } else {
- audioContextReady = true;
- window.removeEventListener('click', resumeAudioContext);
- window.removeEventListener('keydown', resumeAudioContext);
- resolve();
- }
- }
- window.addEventListener('click', resumeAudioContext);
- window.addEventListener('keydown', resumeAudioContext);
- });
- return audioContextPromise;
- }
- async function createBeepSound() {
- if (!(await getSetting("beepOnYou"))) {
- return;
- }
- await ensureAudioContextReady();
- return function playBeep() {
- try {
- const oscillator = audioContext.createOscillator();
- const gainNode = audioContext.createGain();
- oscillator.type = 'sine';
- oscillator.frequency.value = 550;
- gainNode.gain.value = 0.1;
- oscillator.connect(gainNode);
- gainNode.connect(audioContext.destination);
- oscillator.start();
- setTimeout(() => {
- oscillator.stop();
- }, 100);
- } catch (e) {
- console.warn("Beep failed:", e);
- }
- };
- }
- window.originalTitle = document.title;
- window.isNotifying = false;
- let beepOnYouSetting = false;
- let notifyOnYouSetting = false;
- let customMsgSetting = "(!) ";
- let previousFaviconState = null;
- async function initSettings() {
- beepOnYouSetting = await getSetting("beepOnYou");
- notifyOnYouSetting = await getSetting("notifyOnYou");
- const customMsg = await getSetting("notifyOnYou_customMessage");
- if (customMsg) customMsgSetting = customMsg;
- }
- await initSettings();
- let playBeep = null;
- createBeepSound().then(fn => { playBeep = fn; });
- let scrollHandlerActive = false;
- async function notifyOnYou() {
- if (!window.isNotifying) {
- window.isNotifying = true;
- document.title = customMsgSetting + " " + window.originalTitle;
- if (await getSetting("customFavicon")) {
- const { style, state } = faviconManager.getCurrentFaviconState();
- if (state !== "notif") {
- previousFaviconState = { style, state };
- }
- faviconManager.setFaviconStyle(style, "notif");
- }
- }
- }
- function setupNotificationScrollHandler() {
- if (scrollHandlerActive) return;
- scrollHandlerActive = true;
- const BOTTOM_OFFSET = 45;
- function checkScrollPosition() {
- if (!window.isNotifying) return;
- const scrollPosition = window.scrollY + window.innerHeight;
- const documentHeight = document.documentElement.scrollHeight;
- if (scrollPosition >= documentHeight - BOTTOM_OFFSET) {
- document.title = window.originalTitle;
- window.isNotifying = false;
- const { state } = faviconManager.getCurrentFaviconState();
- if (state === "notif" && previousFaviconState) {
- faviconManager.setFaviconStyle(previousFaviconState.style, previousFaviconState.state);
- previousFaviconState = null;
- } else if (state === "notif") {
- faviconManager.setFavicon("base");
- }
- window.removeEventListener('scroll', checkScrollPosition);
- scrollHandlerActive = false;
- }
- }
- window.addEventListener('scroll', checkScrollPosition);
- }
- window.addEventListener("focus", () => {
- if (window.isNotifying) {
- setupNotificationScrollHandler();
- }
- });
- const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: false });
- if (divPostsObs) {
- divPostsObs.addHandler(function beepOnYouHandler(mutations) {
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- if (
- node.nodeType === 1 &&
- typeof node.matches === "function" &&
- (node.matches('.postCell') || node.matches('.opCell')) &&
- node.querySelector("a.quoteLink.you") &&
- !node.closest('.innerPost')
- ) {
- if (beepOnYouSetting && playBeep) {
- playBeep();
- }
- if (notifyOnYouSetting) {
- notifyOnYou();
- setupNotificationScrollHandler();
- }
- }
- }
- }
- });
- }
- window.addEventListener("8chanSS_settingChanged", async (e) => {
- if (e.detail && e.detail.key) {
- const key = e.detail.key;
- if (key === "beepOnYou") {
- beepOnYouSetting = await getSetting("beepOnYou");
- createBeepSound().then(fn => { playBeep = fn; });
- } else if (key === "notifyOnYou") {
- notifyOnYouSetting = await getSetting("notifyOnYou");
- } else if (key === "notifyOnYou_customMessage") {
- const customMsg = await getSetting("notifyOnYou_customMessage");
- if (customMsg) customMsgSetting = customMsg;
- }
- }
- });
- })();
- async function enhanceYouTubeLinks() {
- if (!(window.pageType?.isThread || window.pageType?.isIndex)) {
- return;
- }
- const ytThumbsEnabled = await getSetting("enhanceYoutube_ytThumbs");
- const ytTitleCache = {};
- const MAX_CACHE_SIZE = 350;
- const ORDER_KEY = "_order";
- const TRACKING_PARAMS = [
- "si", "feature", "ref", "fsi", "source",
- "utm_source", "utm_medium", "utm_campaign", "gclid", "gclsrc", "fbclid"
- ];
- const ytThumbCache = {};
- function loadCache() {
- try {
- const data = localStorage.getItem('ytTitleCache');
- if (data) {
- const parsed = JSON.parse(data);
- Object.assign(ytTitleCache, parsed);
- if (!Array.isArray(ytTitleCache[ORDER_KEY])) {
- ytTitleCache[ORDER_KEY] = [];
- }
- } else {
- ytTitleCache[ORDER_KEY] = [];
- }
- } catch (e) {
- ytTitleCache[ORDER_KEY] = [];
- }
- }
- function saveCache() {
- try {
- localStorage.setItem('ytTitleCache', JSON.stringify(ytTitleCache));
- } catch (e) { }
- }
- loadCache();
- function getYouTubeId(url) {
- try {
- const u = new URL(url);
- if (u.hostname === 'youtu.be') {
- return u.pathname.slice(1);
- }
- if (u.hostname.endsWith('youtube.com')) {
- if (u.pathname === '/watch') {
- return u.searchParams.get('v');
- }
- const liveMatch = u.pathname.match(/^\/(live|embed|shorts)\/([a-zA-Z0-9_-]{11})/);
- if (liveMatch) {
- return liveMatch[2];
- }
- }
- } catch (e) { }
- return null;
- }
- function sanitizeYouTubeId(videoId) {
- if (!videoId) return null;
- const match = videoId.match(/([a-zA-Z0-9_-]{11})/);
- return match ? match[1] : null;
- }
- function stripTrackingParams(url) {
- try {
- const u = new URL(url);
- let changed = false;
- const KEEP_PARAMS = new Set(['t', 'start']);
- TRACKING_PARAMS.forEach(param => {
- if (u.searchParams.has(param) && !KEEP_PARAMS.has(param)) {
- u.searchParams.delete(param);
- changed = true;
- }
- });
- if (u.hash && u.hash.includes('?')) {
- const [hashPath, hashQuery] = u.hash.split('?');
- const hashParams = new URLSearchParams(hashQuery);
- let hashChanged = false;
- TRACKING_PARAMS.forEach(param => {
- if (hashParams.has(param) && !KEEP_PARAMS.has(param)) {
- hashParams.delete(param);
- hashChanged = true;
- }
- });
- if (hashChanged) {
- u.hash = hashParams.toString()
- ? `${hashPath}?${hashParams.toString()}`
- : hashPath;
- changed = true;
- }
- }
- return changed ? u.toString() : url;
- } catch (e) {
- return url;
- }
- }
- async function fetchYouTubeTitle(videoId) {
- const cleanId = sanitizeYouTubeId(videoId);
- if (!cleanId) return null;
- if (ytTitleCache.hasOwnProperty(cleanId)) {
- const idx = ytTitleCache[ORDER_KEY].indexOf(cleanId);
- if (idx !== -1) {
- ytTitleCache[ORDER_KEY].splice(idx, 1);
- }
- ytTitleCache[ORDER_KEY].push(cleanId);
- saveCache();
- return ytTitleCache[cleanId];
- }
- try {
- const r = await fetch(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${cleanId}&format=json`);
- if (!r.ok) return null;
- const data = await r.json();
- const title = data ? data.title : null;
- if (title) {
- ytTitleCache[cleanId] = title;
- ytTitleCache[ORDER_KEY].push(cleanId);
- while (ytTitleCache[ORDER_KEY].length > MAX_CACHE_SIZE) {
- const oldest = ytTitleCache[ORDER_KEY].shift();
- delete ytTitleCache[oldest];
- }
- saveCache();
- }
- return title;
- } catch {
- return null;
- }
- }
- async function fetchAsDataURL(url) {
- return new Promise((resolve) => {
- GM.xmlHttpRequest({
- method: "GET",
- url: url,
- responseType: "blob",
- timeout: 8000,
- onload: (response) => {
- if (response.status === 200 && response.response) {
- const reader = new FileReader();
- reader.onloadend = () => resolve(reader.result);
- reader.onerror = () => resolve(null);
- reader.readAsDataURL(response.response);
- } else {
- resolve(null);
- }
- },
- onerror: () => resolve(null),
- ontimeout: () => resolve(null)
- });
- });
- }
- async function fetchYouTubeThumbnailAsDataURL(videoId) {
- if (ytThumbCache.hasOwnProperty(videoId)) {
- return ytThumbCache[videoId];
- }
- const webpUrl = `https://i.ytimg.com/vi_webp/${videoId}/hqdefault.webp`;
- const jpgUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
- let dataUrl = await fetchAsDataURL(webpUrl);
- if (!dataUrl) {
- dataUrl = await fetchAsDataURL(jpgUrl);
- }
- ytThumbCache[videoId] = dataUrl;
- return dataUrl;
- }
- function addThumbnailHover(link, videoId) {
- if (link.dataset.ytThumbHover) return;
- link.dataset.ytThumbHover = "1";
- let thumbDiv = null;
- let lastImg = null;
- let lastHoverToken = 0;
- function showThumb(e) {
- if (!thumbDiv) {
- thumbDiv = document.createElement('div');
- thumbDiv.style.position = 'fixed';
- thumbDiv.style.zIndex = 9999;
- thumbDiv.style.pointerEvents = 'none';
- thumbDiv.style.background = '#222';
- thumbDiv.style.border = '1px solid #444';
- thumbDiv.style.padding = '2px';
- thumbDiv.style.borderRadius = '4px';
- thumbDiv.style.boxShadow = '0 2px 8px rgba(0,0,0,0.4)';
- thumbDiv.style.transition = 'opacity 0.1s';
- thumbDiv.style.opacity = '0';
- thumbDiv.style.maxWidth = '280px';
- thumbDiv.style.maxHeight = '200px';
- const img = document.createElement('img');
- img.style.display = 'block';
- img.style.maxWidth = '280px';
- img.style.maxHeight = '200px';
- img.style.borderRadius = '3px';
- img.alt = "YouTube thumbnail";
- img.src = "data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQv///wAAACH5BAEAAAMALAAAAAAQABAAAAIgjI+py+0Po5yUFQA7";
- lastImg = img;
- const hoverToken = ++lastHoverToken;
- fetchYouTubeThumbnailAsDataURL(videoId).then(dataUrl => {
- if (lastImg === img && hoverToken === lastHoverToken) {
- if (dataUrl) {
- img.src = dataUrl;
- } else {
- img.alt = "Failed to load thumbnail";
- }
- }
- });
- thumbDiv.appendChild(img);
- document.body.appendChild(thumbDiv);
- setTimeout(() => {
- if (thumbDiv) thumbDiv.style.opacity = '1';
- }, 10);
- }
- const top = Math.min(window.innerHeight - 130, e.clientY + 12);
- const left = Math.min(window.innerWidth - 290, e.clientX + 12);
- thumbDiv.style.top = `${top}px`;
- thumbDiv.style.left = `${left}px`;
- }
- function moveThumb(e) {
- if (thumbDiv) {
- const top = Math.min(window.innerHeight - 130, e.clientY + 12);
- const left = Math.min(window.innerWidth - 290, e.clientX + 12);
- thumbDiv.style.top = `${top}px`;
- thumbDiv.style.left = `${left}px`;
- }
- }
- function hideThumb() {
- lastHoverToken++;
- if (thumbDiv && thumbDiv.parentNode) {
- thumbDiv.parentNode.removeChild(thumbDiv);
- thumbDiv = null;
- }
- lastImg = null;
- }
- link.addEventListener('mouseenter', showThumb);
- link.addEventListener('mousemove', moveThumb);
- link.addEventListener('mouseleave', hideThumb);
- }
- function processLinks(root = document) {
- root.querySelectorAll('a[href*="youtu"]').forEach(link => {
- if (link.dataset.ytEnhanced) return;
- const videoId = getYouTubeId(link.href);
- const cleanId = sanitizeYouTubeId(videoId);
- if (!cleanId) return;
- link.dataset.ytEnhanced = "1";
- const cleanUrl = stripTrackingParams(link.href);
- if (cleanUrl !== link.href) {
- link.href = cleanUrl;
- }
- fetchYouTubeTitle(cleanId).then(title => {
- if (title) {
- 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}`;
- }
- });
- if (ytThumbsEnabled) {
- addThumbnailHover(link, cleanId);
- }
- });
- }
- processLinks(document);
- const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
- if (divThreadsObs) {
- divThreadsObs.addHandler(function enhanceYoutubeLinksHandler(mutations) {
- for (const mutation of mutations) {
- for (const addedNode of mutation.addedNodes) {
- if (addedNode.nodeType === 1) {
- processLinks(addedNode);
- }
- }
- }
- });
- }
- }
- function featureLabelCreated12h() {
- if (window.pageType?.isCatalog) {
- return;
- }
- function convertLabelCreatedSpan(span) {
- if (span.dataset.timeConverted === "1") return;
- const text = span.textContent;
- const match = text.match(/^(.+\))\s+(\d{2}):(\d{2}):(\d{2})$/);
- if (!match) return;
- const [_, datePart, hourStr, minStr, secStr] = match;
- let hour = parseInt(hourStr, 10);
- const min = minStr;
- const sec = secStr;
- const ampm = hour >= 12 ? 'PM' : 'AM';
- let hour12 = hour % 12;
- if (hour12 === 0) hour12 = 12;
- const newText = `${datePart} ${hour12}:${min}:${sec} ${ampm}`;
- span.textContent = newText;
- span.dataset.timeConverted = "1";
- }
- function convertAllLabelCreated(root = document) {
- const spans = root.querySelectorAll
- ? root.querySelectorAll('.labelCreated')
- : [];
- spans.forEach(convertLabelCreatedSpan);
- }
- convertAllLabelCreated();
- const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: true });
- if (divPostsObs) {
- divPostsObs.addHandler(function labelCreated12hHandler(mutations) {
- for (const mutation of mutations) {
- for (const addedNode of mutation.addedNodes) {
- if (addedNode.nodeType !== 1) continue;
- if (addedNode.classList && addedNode.classList.contains('labelCreated')) {
- convertLabelCreatedSpan(addedNode);
- } else if (addedNode.querySelectorAll) {
- addedNode.querySelectorAll('.labelCreated').forEach(convertLabelCreatedSpan);
- }
- }
- }
- });
- }
- }
- function truncateFilenames(filenameLength) {
- if (window.pageType?.isCatalog) return;
- if (!divThreads) return;
- function processLinks(root = document) {
- const links = root.querySelectorAll('a.originalNameLink');
- links.forEach(link => {
- if (link.dataset.truncated === "1") return;
- const fullFilename = link.getAttribute('download');
- if (!fullFilename) return;
- const lastDot = fullFilename.lastIndexOf('.');
- if (lastDot === -1) return;
- const name = fullFilename.slice(0, lastDot);
- const ext = fullFilename.slice(lastDot);
- let truncated = fullFilename;
- if (name.length > filenameLength) {
- truncated = name.slice(0, filenameLength) + '(...)' + ext;
- }
- link.textContent = truncated;
- link.dataset.truncated = "1";
- link.dataset.fullFilename = fullFilename;
- link.dataset.truncatedFilename = truncated;
- link.title = fullFilename;
- });
- }
- processLinks(document);
- divThreads.addEventListener('mouseover', function (e) {
- const link = e.target.closest('a.originalNameLink');
- if (link && link.dataset.fullFilename) {
- link.textContent = link.dataset.fullFilename;
- }
- });
- divThreads.addEventListener('mouseout', function (e) {
- const link = e.target.closest('a.originalNameLink');
- if (link && link.dataset.truncatedFilename) {
- link.textContent = link.dataset.truncatedFilename;
- }
- });
- const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
- if (divThreadsObs) {
- const debouncedProcess = debounce(() => processLinks(divThreads), 100);
- divThreadsObs.addHandler(function truncateFilenamesHandler() {
- debouncedProcess();
- });
- }
- }
- function threadInfoHeader() {
- const navHeader = document.querySelector('.navHeader');
- const navOptionsSpan = document.getElementById('navOptionsSpan');
- const postCountEl = document.getElementById('postCount');
- const userCountEl = document.getElementById('userCountLabel');
- const fileCountEl = document.getElementById('fileCount');
- if (!(navHeader && navOptionsSpan && postCountEl && userCountEl && fileCountEl)) return;
- function updateHeader() {
- const postCount = postCountEl.textContent || '0';
- const userCount = userCountEl.textContent || '0';
- const fileCount = fileCountEl.textContent || '0';
- let statsDisplay = navHeader.querySelector('.thread-stats-display');
- if (!statsDisplay) {
- statsDisplay = document.createElement('span');
- statsDisplay.className = 'thread-stats-display';
- statsDisplay.style.marginRight = '1px';
- }
- statsDisplay.innerHTML = `
- [
- <span class="statLabel">Posts: </span><span class="statNumb">${postCount}</span> |
- <span class="statLabel">Users: </span><span class="statNumb">${userCount}</span> |
- <span class="statLabel">Files: </span><span class="statNumb">${fileCount}</span>
- ]
- `;
- if (statsDisplay.parentNode && statsDisplay.parentNode !== navOptionsSpan) {
- statsDisplay.parentNode.removeChild(statsDisplay);
- }
- if (navOptionsSpan.firstChild !== statsDisplay) {
- navOptionsSpan.insertBefore(statsDisplay, navOptionsSpan.firstChild);
- }
- }
- updateHeader();
- const statSelectors = ['#postCount', '#userCountLabel', '#fileCount'];
- statSelectors.forEach(selector => {
- const statObs = observeSelector(selector, { childList: true, subtree: false, characterData: true });
- if (statObs) {
- const debouncedUpdate = debounce(updateHeader, 100);
- statObs.addHandler(function threadInfoHeaderHandler() {
- debouncedUpdate();
- });
- }
- });
- }
- function mediaViewerPositioning() {
- localStorage.setItem("mediaViewer", "true");
- async function updateMediaViewerClass() {
- const mediaViewer = document.getElementById('media-viewer');
- if (!mediaViewer) return;
- const isEnabled = await getSetting("enableMediaViewer");
- if (!isEnabled) {
- mediaViewer.classList.remove('topright', 'topleft');
- return;
- }
- const viewerStyle = await getSetting("enableMediaViewer_viewerStyle");
- mediaViewer.classList.remove('topright', 'topleft');
- if (viewerStyle === 'topright' || viewerStyle === 'topleft') {
- mediaViewer.classList.add(viewerStyle);
- }
- }
- updateMediaViewerClass();
- const mediaViewerObs = observeSelector('#media-viewer', { childList: false, subtree: false });
- if (mediaViewerObs) {
- mediaViewerObs.addHandler(function mediaViewerPositioningHandler() {
- updateMediaViewerClass();
- });
- }
- const bodyObs = observeSelector('body', { childList: true, subtree: false });
- if (bodyObs) {
- bodyObs.addHandler(function bodyMediaViewerHandler(mutations) {
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- if (node.nodeType === 1 && node.id === 'media-viewer') {
- updateMediaViewerClass();
- }
- }
- }
- });
- }
- }
- async function featureHighlightNewIds() {
- if (window.pageType?.isLast || window.pageType?.isCatalog) {
- return;
- }
- const hlStyle = await getSetting("highlightNewIds_idHlStyle");
- if (!divPosts) return;
- if (!document.querySelector('.spanId')) return;
- const styleClassMap = {
- moetext: "moeText",
- glow: "id-glow",
- dotted: "id-dotted"
- };
- const styleClass = styleClassMap[hlStyle] || "moeText";
- function highlightIds(root = divPosts) {
- const idFrequency = {};
- const labelSpans = root.querySelectorAll('.labelId');
- labelSpans.forEach(span => {
- const id = span.textContent.split(/[|\(]/)[0].trim();
- idFrequency[id] = (idFrequency[id] || 0) + 1;
- });
- const seen = {};
- labelSpans.forEach(span => {
- const id = span.textContent.split(/[|\(]/)[0].trim();
- span.classList.remove('moetext', 'id-glow', 'id-dotted');
- if (!seen[id]) {
- seen[id] = true;
- span.classList.add(styleClass);
- span.title = idFrequency[id] === 1
- ? "This ID appears only once."
- : "This was the first occurrence of this ID.";
- } else {
- span.title = "";
- }
- });
- }
- highlightIds();
- const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: true });
- if (divPostsObs) {
- const debouncedHighlightIds = debounce(() => highlightIds(), 50);
- divPostsObs.addHandler(function highlightNewIdsHandler(mutations) {
- let needsUpdate = false;
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- if (
- node.nodeType === 1 &&
- (node.matches?.('.labelId') || node.querySelector?.('.labelId'))
- ) {
- needsUpdate = true;
- break;
- }
- }
- if (needsUpdate) break;
- }
- if (needsUpdate) {
- debouncedHighlightIds();
- }
- });
- }
- }
- async function featureShowIDCount() {
- if (!window.pageType?.isThread) return;
- if (!document.querySelector('.spanId')) return;
- const alwaysShowIdCount = await getSetting("alwaysShowIdCount");
- function updateIdCounts(root = divPosts) {
- const idFrequency = {};
- const labelSpans = root.querySelectorAll('.labelId');
- labelSpans.forEach(span => {
- const id = span.textContent.split(/[|\(]/)[0].trim();
- idFrequency[id] = (idFrequency[id] || 0) + 1;
- });
- labelSpans.forEach(span => {
- const id = span.textContent.split(/[|\(]/)[0].trim();
- if (alwaysShowIdCount) {
- span.textContent = `${id} | ${idFrequency[id]}`;
- span.onmouseover = function (e) { e.stopImmediatePropagation(); e.preventDefault(); };
- span.onmouseout = function (e) { e.stopImmediatePropagation(); e.preventDefault(); };
- } else {
- span.textContent = id;
- span.onmouseover = null;
- span.onmouseout = null;
- }
- });
- }
- updateIdCounts();
- const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: true });
- if (divPostsObs) {
- divPostsObs.addHandler(function showIdCountHandler(mutations) {
- updateIdCounts();
- });
- }
- }
- async function featureQuoteThreading() {
- const isEnabled = typeof getSetting === "function"
- ? await getSetting("quoteThreading")
- : true;
- if (!isEnabled) {
- document.querySelector('.quoteThreadingRefresh')?.remove();
- return;
- }
- function processPosts(posts) {
- posts.forEach(post => {
- const backlinks = post.querySelectorAll('.panelBacklinks .backLink.postLink');
- backlinks.forEach(backlink => {
- const targetUri = backlink.getAttribute('data-target-uri');
- if (!targetUri) return;
- const targetPostId = targetUri.split('#')[1];
- const targetPost = document.getElementById(targetPostId);
- if (targetPost) {
- let repliesContainer = post.nextElementSibling;
- if (!repliesContainer?.classList.contains('threadedReplies')) {
- repliesContainer = document.createElement('div');
- repliesContainer.className = 'threadedReplies';
- post.parentNode.insertBefore(repliesContainer, post.nextSibling);
- }
- if (!repliesContainer.contains(targetPost)) {
- repliesContainer.appendChild(targetPost);
- }
- }
- });
- });
- }
- function threadAllPosts() {
- processPosts(document.querySelectorAll('.divPosts .postCell'));
- }
- function threadNewPosts() {
- const allPosts = document.querySelectorAll('.divPosts .postCell');
- processPosts(Array.from(allPosts).slice(-5));
- }
- const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: false });
- if (divPostsObs) {
- divPostsObs.addHandler(function quoteThreadingHandler(mutations) {
- for (const mutation of mutations) {
- if (mutation.addedNodes.length) {
- setTimeout(threadNewPosts, 50);
- }
- }
- });
- }
- function addRefreshButton() {
- const replyButton = document.querySelector('.threadBottom .innerUtility #replyButton');
- if (!replyButton || replyButton.nextElementSibling?.classList.contains('quoteThreadingBtn')) return;
- const refreshBtn = document.createElement('a');
- refreshBtn.href = "#";
- refreshBtn.className = "quoteThreadingBtn";
- refreshBtn.title = "Refresh quote threading";
- refreshBtn.textContent = "ReThread";
- replyButton.after(' ', refreshBtn);
- refreshBtn.addEventListener('click', e => {
- e.preventDefault();
- threadAllPosts();
- });
- }
- threadAllPosts();
- addRefreshButton();
- }
- function featureLastFifty() {
- if (!window.pageType?.isCatalog) return;
- if (!catalogDiv) return;
- function addLastLinkButtons(root = document) {
- root.querySelectorAll('.catalogCell').forEach(cell => {
- const linkThumb = cell.querySelector('.linkThumb');
- const threadStats = cell.querySelector('.threadStats');
- if (!linkThumb || !threadStats) return;
- const href = linkThumb.getAttribute('href');
- if (!href || !/\/res\//.test(href)) return;
- const lastHref = href.replace('/res/', '/last/');
- threadStats.querySelectorAll('.last-link-btn').forEach(btn => btn.remove());
- const span = document.createElement('span');
- span.className = 'last-link-btn';
- span.style.marginLeft = '0.5em';
- const a = document.createElement('a');
- a.href = lastHref;
- a.textContent = '[L]';
- a.title = 'Go to last 50 posts of this thread';
- a.style.textDecoration = 'none';
- a.style.fontWeight = 'bold';
- span.appendChild(a);
- const labelPage = threadStats.querySelector('.labelPage');
- if (labelPage && labelPage.parentNode) {
- labelPage.parentNode.insertBefore(span, labelPage.nextSibling);
- } else {
- threadStats.appendChild(span);
- }
- });
- }
- addLastLinkButtons(document);
- const catalogDivObs = observeSelector('.catalogDiv', { childList: true, subtree: false });
- if (catalogDivObs) {
- const debouncedUpdate = debounce(() => addLastLinkButtons(document), 50);
- catalogDivObs.addHandler(function lastFiftyHandler() {
- debouncedUpdate();
- });
- }
- }
- function featureToggleIdAsYours() {
- if (!window.pageType?.isThread) return;
- if (!document.querySelector('.spanId')) return;
- function getBoardName() {
- const postCell = document.querySelector('.postCell[data-boarduri], .opCell[data-boarduri]');
- if (postCell) return postCell.getAttribute('data-boarduri');
- const match = location.pathname.match(/^\/([^\/]+)\//);
- return match ? match[1] : 'unknown';
- }
- const BOARD_NAME = getBoardName();
- const T_YOUS_KEY = `${BOARD_NAME}-yous`;
- const MENU_ENTRY_CLASS = "toggleIdAsYoursMenuEntry";
- const MENU_SELECTOR = ".floatingList.extraMenu";
- function getYourPostNumbers() {
- try {
- const val = localStorage.getItem(T_YOUS_KEY);
- return val ? JSON.parse(val).map(Number) : [];
- } catch {
- return [];
- }
- }
- function setYourPostNumbers(arr) {
- localStorage.setItem(T_YOUS_KEY, JSON.stringify(arr.map(Number)));
- }
- document.body.addEventListener('click', function (e) {
- if (e.target.matches('.extraMenuButton')) {
- const postCell = e.target.closest('.postCell, .opCell');
- if (!postCell) return;
- setTimeout(() => {
- let menu = e.target.parentNode.querySelector('.floatingList.extraMenu');
- if (!menu) {
- const menus = Array.from(document.querySelectorAll('.floatingList.extraMenu'));
- menu = menus[menus.length - 1];
- }
- if (menu) {
- menu.setAttribute('data-post-id', postCell.id);
- const labelIdSpan = postCell.querySelector('.labelId');
- if (labelIdSpan) {
- menu.setAttribute('data-label-id', labelIdSpan.textContent.trim());
- }
- addMenuEntries(menu.parentNode || menu);
- }
- }, 0);
- }
- });
- function getLabelIdFromMenu(menu) {
- return menu.getAttribute('data-label-id') || null;
- }
- function toggleYouNameClassForId(labelId, add) {
- document.querySelectorAll('.postCell, .opCell').forEach(postCell => {
- const labelIdSpan = postCell.querySelector('.labelId');
- const rawId = labelIdSpan ? labelIdSpan.textContent.split(/[|\(]/)[0].trim() : null;
- if (rawId === labelId) {
- const nameLink = postCell.querySelector(".linkName.noEmailName");
- if (nameLink) {
- nameLink.classList.toggle("youName", add);
- }
- }
- });
- }
- function getAllPostNumbersForId(labelId) {
- const postNumbers = [];
- document.querySelectorAll('.divPosts .postCell').forEach(postCell => {
- const labelIdSpan = postCell.querySelector('.labelId');
- const rawId = labelIdSpan ? labelIdSpan.textContent.split(/[|\(]/)[0].trim() : null;
- if (rawId === labelId) {
- const num = Number(postCell.id);
- if (!isNaN(num)) postNumbers.push(num);
- }
- });
- return postNumbers;
- }
- function addMenuEntries(root = document) {
- root.querySelectorAll(MENU_SELECTOR).forEach(menu => {
- if (!menu.closest('.extraMenuButton')) return;
- const ul = menu.querySelector("ul");
- if (!ul || ul.querySelector("." + MENU_ENTRY_CLASS)) return;
- let labelId = getLabelIdFromMenu(menu);
- if (!labelId) return;
- labelId = labelId.split(/[|\(]/)[0].trim();
- const yourPostNumbers = getYourPostNumbers();
- const postNumbersForId = getAllPostNumbersForId(labelId);
- const isMarked = postNumbersForId.length > 0 && postNumbersForId.every(num => yourPostNumbers.includes(num));
- const li = document.createElement("li");
- li.className = MENU_ENTRY_CLASS;
- li.style.cursor = "pointer";
- li.textContent = "Toggle ID as Yours";
- ul.appendChild(li);
- li.addEventListener("click", function (e) {
- e.stopPropagation();
- let labelId = getLabelIdFromMenu(menu);
- if (!labelId) return;
- labelId = labelId.split(/[|\(]/)[0].trim();
- let yourPostNumbers = getYourPostNumbers();
- const postNumbersForId = getAllPostNumbersForId(labelId);
- if (postNumbersForId.length === 0) return;
- const allMarked = postNumbersForId.every(num => yourPostNumbers.includes(num));
- if (!allMarked) {
- postNumbersForId.forEach(num => {
- if (!yourPostNumbers.includes(num)) yourPostNumbers.push(num);
- });
- setYourPostNumbers(yourPostNumbers);
- toggleYouNameClassForId(labelId, true);
- } else {
- yourPostNumbers = yourPostNumbers.filter(num => !postNumbersForId.includes(num));
- setYourPostNumbers(yourPostNumbers);
- toggleYouNameClassForId(labelId, false);
- }
- });
- toggleYouNameClassForId(labelId, isMarked);
- });
- }
- window.addEventListener("storage", function (event) {
- if (event.key === T_YOUS_KEY) {
- const yourPostNumbers = getYourPostNumbers();
- document.querySelectorAll('.postCell, .opCell').forEach(postCell => {
- const nameLink = postCell.querySelector(".linkName.noEmailName");
- if (nameLink) {
- const postNum = Number(postCell.id);
- nameLink.classList.toggle("youName", yourPostNumbers.includes(postNum));
- }
- });
- }
- });
- const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
- if (divThreadsObs) {
- const debouncedObserverCallback = debounce((mutations) => {
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- if (node.nodeType !== 1) continue;
- if (node.matches && node.matches(MENU_SELECTOR)) {
- if (!node.hasAttribute('data-label-id')) {
- const btn = node.closest('.extraMenuButton');
- const postCell = btn && btn.closest('.postCell, .opCell');
- if (postCell) {
- const labelIdSpan = postCell.querySelector('.labelId');
- if (labelIdSpan) {
- node.setAttribute('data-label-id', labelIdSpan.textContent.trim());
- }
- }
- }
- addMenuEntries(node.parentNode || node);
- } else if (node.querySelectorAll) {
- node.querySelectorAll(MENU_SELECTOR).forEach(menu => {
- if (!menu.hasAttribute('data-label-id')) {
- const btn = menu.closest('.extraMenuButton');
- const postCell = btn && btn.closest('.postCell, .opCell');
- if (postCell) {
- const labelIdSpan = postCell.querySelector('.labelId');
- if (labelIdSpan) {
- menu.setAttribute('data-label-id', labelIdSpan.textContent.trim());
- }
- }
- }
- addMenuEntries(menu.parentNode || menu);
- });
- }
- }
- }
- }, 100);
- divThreadsObs.addHandler(debouncedObserverCallback);
- }
- const yourPostNumbers = getYourPostNumbers();
- document.querySelectorAll('.postCell, .opCell').forEach(postCell => {
- const nameLink = postCell.querySelector(".linkName.noEmailName");
- if (nameLink) {
- const postNum = Number(postCell.id);
- nameLink.classList.toggle("youName", yourPostNumbers.includes(postNum));
- }
- });
- }
- async function featureSauceLinks() {
- if (!(window.pageType?.isThread || window.pageType?.isIndex)) {
- return;
- }
- const enabled = await getSetting("enableTheSauce");
- if (!enabled) return;
- const services = [
- {
- key: "iqdb",
- label: "iqdb",
- enabled: await getSetting("enableTheSauce_iqdb"),
- method: "post",
- url: "https://iqdb.org/",
- fileField: "file",
- },
- {
- key: "saucenao",
- label: "sauce",
- enabled: await getSetting("enableTheSauce_saucenao"),
- method: "post",
- url: "https://saucenao.com/search.php",
- fileField: "file",
- },
- {
- key: "pixiv",
- label: "pixiv",
- enabled: await getSetting("enableTheSauce_pixiv"),
- method: "pixiv",
- },
- ];
- function getImageUrl(detailDiv) {
- const parentCell = detailDiv.closest('.postCell') || detailDiv.closest('.opCell');
- const imgLink = parentCell?.querySelector('.imgLink');
- const img = imgLink ? imgLink.querySelector('img') : null;
- if (!img) {
- return null;
- }
- let imgSrc = img.getAttribute('src');
- let origin = window.location.origin;
- if (imgSrc.startsWith("//")) {
- return window.location.protocol + imgSrc;
- } else if (imgSrc.startsWith("/")) {
- return origin + imgSrc;
- } else if (/^https?:\/\//.test(imgSrc)) {
- return imgSrc;
- } else {
- return origin + "/" + imgSrc;
- }
- }
- async function fetchImageBlob(url) {
- const response = await fetch(url);
- if (!response.ok) throw new Error("Failed to fetch image");
- return await response.blob();
- }
- function getPixivId(detailDiv) {
- const origNameLink = detailDiv.querySelector('.originalNameLink');
- if (!origNameLink) return null;
- const filename = origNameLink.getAttribute('download') || origNameLink.textContent;
- const match = filename && filename.match(/^(\d+)_p\d+\./);
- return match ? match[1] : null;
- }
- function addSauceLinksToElement(detailDiv) {
- if (detailDiv.classList.contains('sauceLinksProcessed')) {
- return;
- }
- detailDiv.querySelectorAll('.sauceLinksContainer').forEach(el => el.remove());
- const imgUrl = getImageUrl(detailDiv);
- if (!imgUrl) {
- return;
- }
- const container = document.createElement('div');
- container.className = 'sauceLinksContainer';
- container.style.marginTop = '3px';
- container.style.display = 'flex';
- container.style.flexWrap = 'wrap';
- container.style.gap = '6px';
- let anyLink = false;
- services.forEach(service => {
- if (!service.enabled) {
- return;
- }
- const a = document.createElement('a');
- a.className = 'sauceLink';
- a.target = '_blank';
- a.style.fontSize = '90%';
- a.textContent = service.label;
- if (service.method === "post") {
- a.href = "#";
- a.title = `Upload thumbnail to ${service.label}`;
- a.addEventListener('click', async (e) => {
- e.preventDefault();
- try {
- const blob = await fetchImageBlob(imgUrl);
- const file = new File([blob], "image.png", { type: blob.type || "image/png" });
- const form = document.createElement("form");
- form.action = service.url;
- form.method = "POST";
- form.enctype = "multipart/form-data";
- form.target = "_blank";
- form.style.display = "none";
- const input = document.createElement("input");
- input.type = "file";
- input.name = service.fileField;
- form.appendChild(input);
- const dt = new DataTransfer();
- dt.items.add(file);
- input.files = dt.files;
- document.body.appendChild(form);
- form.submit();
- setTimeout(() => form.remove(), 10000);
- } catch (err) {
- alert("Failed to upload thumbnail: " + err);
- }
- });
- } else if (service.method === "pixiv") {
- const pixivId = getPixivId(detailDiv);
- if (pixivId) {
- a.href = `https://www.pixiv.net/artworks/${pixivId}`;
- a.title = "Open Pixiv artwork";
- } else {
- return;
- }
- }
- container.appendChild(a);
- anyLink = true;
- });
- if (anyLink) {
- detailDiv.classList.add('sauceLinksProcessed');
- detailDiv.appendChild(container);
- }
- }
- function observeAllUploadDetails(container = document) {
- const details = container.querySelectorAll('.uploadDetails:not(.sauceLinksProcessed)');
- details.forEach(detailDiv => addSauceLinksToElement(detailDiv));
- }
- observeAllUploadDetails();
- const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: true });
- if (divPostsObs) {
- divPostsObs.addHandler(function sauceLinksHandler(mutations) {
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- if (node.nodeType === 1) {
- if (node.classList && node.classList.contains('uploadDetails')) {
- addSauceLinksToElement(node);
- } else if (node.querySelectorAll) {
- node.querySelectorAll('.uploadDetails:not(.sauceLinksProcessed)').forEach(addSauceLinksToElement);
- }
- }
- }
- }
- });
- }
- const bodyObs = observeSelector('body', { childList: true, subtree: true });
- if (bodyObs) {
- bodyObs.addHandler(function quoteTooltipSauceLinksHandler(mutations) {
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- if (node.nodeType !== 1) continue;
- if (node.classList && node.classList.contains('quoteTooltip')) {
- node.querySelectorAll('.uploadDetails:not(.sauceLinksProcessed)').forEach(addSauceLinksToElement);
- } else if (node.classList && node.classList.contains('innerPost')) {
- node.querySelectorAll('.uploadDetails:not(.sauceLinksProcessed)').forEach(addSauceLinksToElement);
- } else if (node.querySelectorAll) {
- node.querySelectorAll('.quoteTooltip .uploadDetails:not(.sauceLinksProcessed)').forEach(addSauceLinksToElement);
- node.querySelectorAll('.innerPost .uploadDetails:not(.sauceLinksProcessed)').forEach(addSauceLinksToElement);
- }
- }
- }
- });
- }
- }
- function featureCustomPostHideMenu() {
- const HIDDEN_POSTS_KEY = '8chanSS_hiddenPosts';
- const FILTERED_NAMES_KEY = '8chanSS_filteredNames';
- const FILTERED_IDS_KEY = '8chanSS_filteredIDs';
- async function getStoredObject(key) {
- let obj = {};
- if (typeof GM !== 'undefined' && GM.getValue) {
- obj = await GM.getValue(key, {});
- }
- return typeof obj === 'object' && obj !== null ? obj : {};
- }
- async function setStoredObject(key, obj) {
- if (typeof GM !== 'undefined' && GM.setValue) {
- await GM.setValue(key, obj);
- }
- }
- function getAllHideButtons(root = document) {
- return Array.from(root.querySelectorAll('label.hideButton'));
- }
- function getPostCellFromButton(btn) {
- return btn.closest('.postCell, .opCell');
- }
- function getInnerPostElem(cell) {
- return cell.querySelector('.innerPost') || cell.querySelector('.innerOP');
- }
- function getThreadIdFromInnerPost(inner) {
- if (!inner) return null;
- const dataUri = inner.getAttribute('data-uri');
- if (!dataUri) return null;
- const parts = dataUri.split('/');
- if (parts.length < 2) return null;
- return parts[1].split('#')[0];
- }
- function getPostId(cell) {
- return cell.id ? cell.id.replace(/\D/g, '') : '';
- }
- function getBoardUri(cell) {
- return cell.getAttribute('data-boarduri') || '';
- }
- function hidePostCellWithStub(cell, boardUri, postId, onUnhide, reason) {
- if (!cell) return;
- const inner = getInnerPostElem(cell);
- if (!inner) return;
- inner.classList.add('hidden');
- const oldStub = cell.querySelector('.unhideButton');
- if (oldStub) oldStub.remove();
- const unhideBtn = document.createElement('span');
- unhideBtn.className = 'unhideButton glowOnHover';
- let stubText = `[Unhide post /${boardUri}/${postId}]`;
- if (reason === 'filteredID') stubText += ' (filtered ID)';
- else if (reason === 'filteredIDPlus') stubText += ' (reply to filtered ID)';
- else if (reason === 'filteredName') stubText += ' (filtered name)';
- else if (reason === 'filteredNamePlus') stubText += ' (reply to filtered name)';
- else if (reason === 'hidePostPlus') stubText += ' (reply to hidden post)';
- unhideBtn.textContent = stubText;
- unhideBtn.addEventListener('click', function (e) {
- e.stopPropagation();
- inner.classList.remove('hidden');
- unhideBtn.remove();
- if (typeof onUnhide === 'function') onUnhide();
- updateAllQuoteLinksFiltered();
- });
- inner.parentNode.insertBefore(unhideBtn, inner.nextSibling);
- }
- function unhidePostCell(cell, boardUri, postId) {
- const inner = getInnerPostElem(cell);
- if (inner) inner.classList.remove('hidden');
- const unhideBtn = cell.querySelector('.unhideButton');
- if (unhideBtn) unhideBtn.remove();
- updateAllQuoteLinksFiltered();
- }
- function getAllRepliesRecursive(rootPostId, boardUri) {
- const postMap = {};
- document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
- const pid = getPostId(cell);
- if (pid) postMap[pid] = cell;
- });
- const toHide = new Set();
- const visited = new Set();
- const rootNum = Number(rootPostId);
- const queue = [];
- queue.push(rootPostId);
- while (queue.length > 0) {
- const currentId = queue.shift();
- const postEl = postMap[currentId];
- if (!postEl) continue;
- const backlinks = postEl.querySelectorAll('.panelBacklinks .backLink[data-target-uri]');
- backlinks.forEach(link => {
- const targetUri = link.getAttribute('data-target-uri');
- const match = targetUri && targetUri.match(/^([^#]+)#(\d+)$/);
- if (match) {
- const replyId = match[2];
- const replyNum = Number(replyId);
- if (!isNaN(replyNum) && replyNum > rootNum && !visited.has(replyId)) {
- toHide.add(replyId);
- visited.add(replyId);
- queue.push(replyId);
- }
- }
- });
- }
- return toHide;
- }
- async function setPostHidden(boardUri, postId, hide = true, plus = false) {
- const recursiveHide = await getSetting("enableHidingMenu_recursiveHide");
- document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
- if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
- if (getPostId(cell) === postId) {
- if (hide) {
- hidePostCellWithStub(cell, boardUri, postId, null, plus ? 'hidePostPlus' : undefined);
- } else {
- unhidePostCell(cell, boardUri, postId);
- }
- }
- });
- if (plus) {
- if (recursiveHide) {
- getAllRepliesRecursive(postId, boardUri).forEach(replyPid => {
- document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
- if (getPostId(cell) === replyPid) {
- if (hide) {
- hidePostCellWithStub(cell, getBoardUri(cell), getPostId(cell), null, 'hidePostPlus');
- } else {
- unhidePostCell(cell, getBoardUri(cell), getPostId(cell));
- }
- }
- });
- });
- } else {
- document.querySelectorAll('.postCell, .opCell').forEach(cell => {
- if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
- const quoteLinks = cell.querySelectorAll('.quoteLink[data-target-uri]');
- for (const link of quoteLinks) {
- const targetUri = link.getAttribute('data-target-uri');
- const match = targetUri && targetUri.match(/^([^#]+)#(\w+)$/);
- if (match && match[2] === postId) {
- if (hide) {
- hidePostCellWithStub(cell, getBoardUri(cell), getPostId(cell), null, 'hidePostPlus');
- } else {
- unhidePostCell(cell, getBoardUri(cell), getPostId(cell));
- }
- break;
- }
- }
- });
- }
- }
- updateAllQuoteLinksFiltered();
- }
- async function setPostsWithNameHidden(name, hide = true, plus = false) {
- const postIdsWithName = new Set();
- document.querySelectorAll('.postCell, .opCell').forEach(cell => {
- if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
- const nameElem = cell.querySelector('.linkName');
- if (nameElem && nameElem.textContent.trim() === name) {
- const boardUri = getBoardUri(cell);
- const postId = getPostId(cell);
- postIdsWithName.add(postId);
- if (hide) {
- hidePostCellWithStub(cell, boardUri, postId, null, plus ? 'filteredNamePlus' : 'filteredName');
- } else {
- unhidePostCell(cell, boardUri, postId);
- }
- }
- });
- if (plus && postIdsWithName.size > 0) {
- document.querySelectorAll('.postCell, .opCell').forEach(cell => {
- if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
- const quoteLinks = cell.querySelectorAll('.quoteLink[data-target-uri]');
- for (const link of quoteLinks) {
- const targetUri = link.getAttribute('data-target-uri');
- const match = targetUri && targetUri.match(/^([^#]+)#(\w+)$/);
- if (match && postIdsWithName.has(match[2])) {
- if (hide) {
- hidePostCellWithStub(cell, getBoardUri(cell), getPostId(cell), null, 'filteredNamePlus');
- } else {
- unhidePostCell(cell, getBoardUri(cell), getPostId(cell));
- }
- break;
- }
- }
- });
- }
- updateAllQuoteLinksFiltered();
- }
- async function setPostsWithIdHidden(boardUri, threadId, id, hide = true, plus = false) {
- const postIdsWithId = new Set();
- if (!/^[a-z0-9]+$/i.test(id)) return;
- document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
- if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
- const inner = getInnerPostElem(cell);
- const cellThreadId = getThreadIdFromInnerPost(inner);
- const idElem = cell.querySelector('.labelId');
- const cellId = idElem ? idElem.textContent.split(/[|\(]/)[0].trim() : null;
- if (
- cellThreadId === threadId &&
- cellId &&
- cellId === id
- ) {
- const postId = getPostId(cell);
- postIdsWithId.add(postId);
- if (hide) {
- hidePostCellWithStub(cell, boardUri, postId, null, plus ? 'filteredIDPlus' : 'filteredID');
- } else {
- unhidePostCell(cell, boardUri, postId);
- }
- }
- });
- if (plus && postIdsWithId.size > 0) {
- document.querySelectorAll('.postCell, .opCell').forEach(cell => {
- if (cell.classList.contains('opCell') || cell.classList.contains('innerOP')) return;
- let isDirectReply = false;
- const quoteLinks = cell.querySelectorAll('.quoteLink[data-target-uri]');
- for (const link of quoteLinks) {
- const targetUri = link.getAttribute('data-target-uri');
- const match = targetUri && targetUri.match(/^([^#]+)#([a-z0-9]+)$/i);
- if (match && postIdsWithId.has(match[2])) {
- isDirectReply = true;
- break;
- }
- }
- if (!isDirectReply) {
- const panelBacklinks = cell.querySelector('.panelBacklinks');
- if (panelBacklinks) {
- const backLinks = panelBacklinks.querySelectorAll('.backLink[data-target-uri]');
- for (const link of backLinks) {
- const targetUri = link.getAttribute('data-target-uri');
- const match = targetUri && targetUri.match(/^([^#]+)#([a-z0-9]+)$/i);
- if (match && postIdsWithId.has(match[2])) {
- isDirectReply = true;
- break;
- }
- }
- }
- }
- if (isDirectReply) {
- const replyBoardUri = getBoardUri(cell);
- const replyPostId = getPostId(cell);
- if (hide) {
- hidePostCellWithStub(cell, replyBoardUri, replyPostId, null, 'filteredIDPlus');
- } else {
- unhidePostCell(cell, replyBoardUri, replyPostId);
- }
- }
- });
- }
- updateAllQuoteLinksFiltered();
- }
- async function updateAllQuoteLinksFiltered() {
- const hiddenPostsObj = await getStoredObject(HIDDEN_POSTS_KEY);
- let filteredNamesObj = await getStoredObject(FILTERED_NAMES_KEY);
- if (!filteredNamesObj || typeof filteredNamesObj !== "object" || Array.isArray(filteredNamesObj)) {
- filteredNamesObj = { simple: Array.isArray(filteredNamesObj) ? filteredNamesObj : [], plus: [] };
- }
- if (!Array.isArray(filteredNamesObj.simple)) filteredNamesObj.simple = [];
- if (!Array.isArray(filteredNamesObj.plus)) filteredNamesObj.plus = [];
- const filteredTargets = new Set();
- for (const boardUri in hiddenPostsObj) {
- for (const postId of (hiddenPostsObj[boardUri]?.simple || [])) {
- filteredTargets.add(`${boardUri}#${postId}`);
- }
- for (const postId of (hiddenPostsObj[boardUri]?.plus || [])) {
- filteredTargets.add(`${boardUri}#${postId}`);
- getAllRepliesRecursive(String(postId), boardUri).forEach(pid => {
- filteredTargets.add(`${boardUri}#${pid}`);
- });
- }
- }
- const filteredIdsObj = await getStoredObject(FILTERED_IDS_KEY);
- for (const boardUri in filteredIdsObj) {
- for (const threadId in filteredIdsObj[boardUri]) {
- let threadObj = filteredIdsObj[boardUri][threadId];
- if (Array.isArray(threadObj)) {
- threadObj = { simple: threadObj, plus: [] };
- filteredIdsObj[boardUri][threadId] = threadObj;
- }
- for (const id of threadObj.simple || []) {
- document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
- const idElem = cell.querySelector('.labelId');
- if (idElem && idElem.textContent.trim() === id) {
- const postId = getPostId(cell);
- filteredTargets.add(`${boardUri}#${postId}`);
- }
- });
- }
- for (const id of threadObj.plus || []) {
- const postIdsWithId = new Set();
- document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
- const idElem = cell.querySelector('.labelId');
- if (idElem && idElem.textContent.trim() === id) {
- const postId = getPostId(cell);
- filteredTargets.add(`${boardUri}#${postId}`);
- postIdsWithId.add(postId);
- }
- });
- if (postIdsWithId.size > 0) {
- document.querySelectorAll('.postCell, .opCell').forEach(cell => {
- const panelBacklinks = cell.querySelector('.panelBacklinks');
- if (!panelBacklinks) return;
- const backLinks = panelBacklinks.querySelectorAll('.backLink[data-target-uri]');
- let isDirectReply = false;
- for (const link of backLinks) {
- const targetUri = link.getAttribute('data-target-uri');
- const match = targetUri && targetUri.match(/^([^#]+)#(\d+)$/);
- if (match && postIdsWithId.has(match[2])) {
- isDirectReply = true;
- break;
- }
- }
- if (isDirectReply) {
- const replyBoardUri = getBoardUri(cell);
- const replyPostId = getPostId(cell);
- filteredTargets.add(`${replyBoardUri}#${replyPostId}`);
- }
- });
- }
- }
- }
- }
- for (const name of filteredNamesObj.simple) {
- document.querySelectorAll('.postCell, .opCell').forEach(cell => {
- const nameElem = cell.querySelector('.linkName');
- if (nameElem && nameElem.textContent.trim() === name) {
- const boardUri = getBoardUri(cell);
- const postId = getPostId(cell);
- filteredTargets.add(`${boardUri}#${postId}`);
- }
- });
- }
- for (const name of filteredNamesObj.plus) {
- const postIdsWithName = new Set();
- document.querySelectorAll('.postCell, .opCell').forEach(cell => {
- const nameElem = cell.querySelector('.linkName');
- if (nameElem && nameElem.textContent.trim() === name) {
- const boardUri = getBoardUri(cell);
- const postId = getPostId(cell);
- filteredTargets.add(`${boardUri}#${postId}`);
- postIdsWithName.add(postId);
- }
- });
- postIdsWithName.forEach(pid => {
- getAllRepliesRecursive(pid).forEach(replyPid => {
- document.querySelectorAll('.postCell, .opCell').forEach(cell => {
- if (getPostId(cell) === replyPid) {
- const boardUri = getBoardUri(cell);
- filteredTargets.add(`${boardUri}#${replyPid}`);
- }
- });
- });
- });
- }
- document.querySelectorAll('.quoteLink').forEach(link => {
- let isFiltered = false;
- const targetUri = link.getAttribute('data-target-uri');
- if (targetUri && filteredTargets.has(targetUri)) {
- isFiltered = true;
- } else {
- const href = link.getAttribute('href');
- if (href) {
- const match = href.match(/\/([^\/]+)\/res\/\d+\.html#(\d+)$/);
- if (match) {
- const boardUri = match[1];
- const postId = match[2];
- if (filteredTargets.has(`${boardUri}#${postId}`)) {
- isFiltered = true;
- }
- }
- }
- }
- if (isFiltered) link.classList.add('filtered');
- else link.classList.remove('filtered');
- });
- }
- async function showCustomMenu(hideButton, postCell) {
- removeExistingMenu();
- const extraMenu = document.createElement('div');
- extraMenu.className = 'floatingList extraMenu';
- extraMenu.setAttribute('data-custom', '1');
- const rect = hideButton.getBoundingClientRect();
- extraMenu.style.position = 'absolute';
- extraMenu.style.left = `${rect.left + window.scrollX}px`;
- extraMenu.style.top = `${rect.bottom + window.scrollY}px`;
- extraMenu.style.zIndex = 9999;
- extraMenu.style.fontSize = "10pt";
- const list = document.createElement('ul');
- extraMenu.appendChild(list);
- const boardUri = getBoardUri(postCell);
- const postId = getPostId(postCell);
- const inner = getInnerPostElem(postCell);
- const threadId = getThreadIdFromInnerPost(inner);
- const idElem = postCell.querySelector('.labelId');
- const id = idElem ? idElem.textContent.split(/[|\(]/)[0].trim() : null;
- const nameElem = postCell.querySelector('.linkName');
- const name = nameElem ? nameElem.textContent.trim() : null;
- const isOP = postCell.classList.contains('opCell') || postCell.classList.contains('innerOP');
- const hiddenPostsObj = await getStoredObject(HIDDEN_POSTS_KEY);
- if (!hiddenPostsObj[boardUri]) hiddenPostsObj[boardUri] = { simple: [], plus: [] };
- if (!Array.isArray(hiddenPostsObj[boardUri].simple)) hiddenPostsObj[boardUri].simple = [];
- if (!Array.isArray(hiddenPostsObj[boardUri].plus)) hiddenPostsObj[boardUri].plus = [];
- const isHiddenSimple = hiddenPostsObj[boardUri].simple.includes(Number(postId));
- const isHiddenPlus = hiddenPostsObj[boardUri].plus.includes(Number(postId));
- const filteredIdsObj = await getStoredObject(FILTERED_IDS_KEY);
- let threadObj = filteredIdsObj[boardUri] && filteredIdsObj[boardUri][threadId];
- if (Array.isArray(threadObj)) {
- threadObj = { simple: threadObj, plus: [] };
- filteredIdsObj[boardUri][threadId] = threadObj;
- } else if (!threadObj) {
- threadObj = { simple: [], plus: [] };
- if (!filteredIdsObj[boardUri]) filteredIdsObj[boardUri] = {};
- filteredIdsObj[boardUri][threadId] = threadObj;
- }
- if (!Array.isArray(threadObj.simple)) threadObj.simple = [];
- if (!Array.isArray(threadObj.plus)) threadObj.plus = [];
- const isFilteredId = id && threadObj.simple.includes(id);
- const isFilteredIdPlus = id && threadObj.plus.includes(id);
- let filteredNamesObj = await getStoredObject(FILTERED_NAMES_KEY);
- if (!filteredNamesObj || typeof filteredNamesObj !== "object" || Array.isArray(filteredNamesObj)) {
- filteredNamesObj = { simple: Array.isArray(filteredNamesObj) ? filteredNamesObj : [], plus: [] };
- }
- if (!Array.isArray(filteredNamesObj.simple)) filteredNamesObj.simple = [];
- if (!Array.isArray(filteredNamesObj.plus)) filteredNamesObj.plus = [];
- const isNameFiltered = name && filteredNamesObj.simple.includes(name);
- const isNameFilteredPlus = name && filteredNamesObj.plus.includes(name);
- const options = [];
- if (!isOP) {
- options.push(
- {
- name: isHiddenSimple ? 'Unhide post' : 'Hide post',
- callback: async () => {
- let obj = await getStoredObject(HIDDEN_POSTS_KEY);
- if (!obj[boardUri]) obj[boardUri] = { simple: [], plus: [] };
- let arr = obj[boardUri].simple;
- const idx = arr.indexOf(Number(postId));
- if (idx !== -1) {
- arr.splice(idx, 1);
- obj[boardUri].simple = arr;
- await setStoredObject(HIDDEN_POSTS_KEY, obj);
- setPostHidden(boardUri, postId, false, false);
- } else {
- arr.push(Number(postId));
- obj[boardUri].simple = arr;
- await setStoredObject(HIDDEN_POSTS_KEY, obj);
- setPostHidden(boardUri, postId, true, false);
- }
- removeExistingMenu();
- }
- },
- {
- name: isHiddenPlus ? 'Unhide post+' : 'Hide post+',
- callback: async () => {
- let obj = await getStoredObject(HIDDEN_POSTS_KEY);
- if (!obj[boardUri]) obj[boardUri] = { simple: [], plus: [] };
- let arr = obj[boardUri].plus;
- const idx = arr.indexOf(Number(postId));
- if (idx !== -1) {
- arr.splice(idx, 1);
- obj[boardUri].plus = arr;
- await setStoredObject(HIDDEN_POSTS_KEY, obj);
- setPostHidden(boardUri, postId, false, true);
- } else {
- arr.push(Number(postId));
- obj[boardUri].plus = arr;
- await setStoredObject(HIDDEN_POSTS_KEY, obj);
- setPostHidden(boardUri, postId, true, true);
- }
- removeExistingMenu();
- }
- }
- );
- }
- options.push(
- {
- name: isNameFiltered ? 'Unfilter name' : 'Filter name',
- callback: async () => {
- let obj = await getStoredObject(FILTERED_NAMES_KEY);
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
- obj = { simple: Array.isArray(obj) ? obj : [], plus: [] };
- }
- if (!Array.isArray(obj.simple)) obj.simple = [];
- const idx = obj.simple.indexOf(name);
- if (idx !== -1) {
- obj.simple.splice(idx, 1);
- await setStoredObject(FILTERED_NAMES_KEY, obj);
- setPostsWithNameHidden(name, false, false);
- } else {
- obj.simple.push(name);
- await setStoredObject(FILTERED_NAMES_KEY, obj);
- setPostsWithNameHidden(name, true, false);
- }
- removeExistingMenu();
- }
- },
- {
- name: isNameFilteredPlus ? 'Unfilter name+' : 'Filter name+',
- callback: async () => {
- let obj = await getStoredObject(FILTERED_NAMES_KEY);
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
- obj = { simple: Array.isArray(obj) ? obj : [], plus: [] };
- }
- if (!Array.isArray(obj.plus)) obj.plus = [];
- const idx = obj.plus.indexOf(name);
- if (idx !== -1) {
- obj.plus.splice(idx, 1);
- await setStoredObject(FILTERED_NAMES_KEY, obj);
- setPostsWithNameHidden(name, false, true);
- } else {
- obj.plus.push(name);
- await setStoredObject(FILTERED_NAMES_KEY, obj);
- setPostsWithNameHidden(name, true, true);
- }
- removeExistingMenu();
- }
- },
- {
- name: isFilteredId ? 'Unfilter ID' : 'Filter ID',
- callback: async () => {
- let obj = await getStoredObject(FILTERED_IDS_KEY);
- if (!obj[boardUri]) obj[boardUri] = {};
- let threadObj = obj[boardUri][threadId];
- if (Array.isArray(threadObj)) {
- threadObj = { simple: threadObj, plus: [] };
- obj[boardUri][threadId] = threadObj;
- } else if (!threadObj) {
- threadObj = { simple: [], plus: [] };
- obj[boardUri][threadId] = threadObj;
- }
- if (!Array.isArray(threadObj.simple)) threadObj.simple = [];
- let arr = threadObj.simple;
- const rawId = id ? id.split(/[|\(]/)[0].trim() : id;
- const idx = arr.indexOf(rawId);
- if (idx !== -1) {
- arr.splice(idx, 1);
- threadObj.simple = arr;
- await setStoredObject(FILTERED_IDS_KEY, obj);
- setPostsWithIdHidden(boardUri, threadId, rawId, false, false);
- } else {
- arr.push(rawId);
- threadObj.simple = arr;
- await setStoredObject(FILTERED_IDS_KEY, obj);
- setPostsWithIdHidden(boardUri, threadId, rawId, true, false);
- }
- removeExistingMenu();
- }
- },
- {
- name: isFilteredIdPlus ? 'Unfilter ID+' : 'Filter ID+',
- callback: async () => {
- let obj = await getStoredObject(FILTERED_IDS_KEY);
- if (!obj[boardUri]) obj[boardUri] = {};
- let threadObj = obj[boardUri][threadId];
- if (Array.isArray(threadObj)) {
- threadObj = { simple: threadObj, plus: [] };
- obj[boardUri][threadId] = threadObj;
- } else if (!threadObj) {
- threadObj = { simple: [], plus: [] };
- obj[boardUri][threadId] = threadObj;
- }
- if (!Array.isArray(threadObj.plus)) threadObj.plus = [];
- let arr = threadObj.plus;
- const rawId = id ? id.split(/[|\(]/)[0].trim() : id;
- const idx = arr.indexOf(rawId);
- if (idx !== -1) {
- arr.splice(idx, 1);
- threadObj.plus = arr;
- await setStoredObject(FILTERED_IDS_KEY, obj);
- setPostsWithIdHidden(boardUri, threadId, rawId, false, true);
- } else {
- arr.push(rawId);
- threadObj.plus = arr;
- await setStoredObject(FILTERED_IDS_KEY, obj);
- setPostsWithIdHidden(boardUri, threadId, rawId, true, true);
- }
- removeExistingMenu();
- }
- }
- );
- options.forEach(opt => {
- const li = document.createElement('li');
- li.textContent = opt.name;
- li.onclick = opt.callback;
- list.appendChild(li);
- });
- document.body.appendChild(extraMenu);
- function handleOutsideClick(e) {
- if (!extraMenu.contains(e.target)) {
- removeExistingMenu();
- document.removeEventListener('mousedown', handleOutsideClick);
- }
- }
- setTimeout(() => {
- document.addEventListener('mousedown', handleOutsideClick);
- }, 0);
- }
- function removeExistingMenu() {
- document.querySelectorAll('.floatingList.extraMenu[data-custom]').forEach(menu => menu.remove());
- }
- function hijackHideButtons(root = document) {
- getAllHideButtons(root).forEach(hideButton => {
- if (hideButton.dataset.customMenuHijacked) return;
- hideButton.dataset.customMenuHijacked = "1";
- hideButton.onclick = null;
- hideButton.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- showCustomMenu(hideButton, getPostCellFromButton(hideButton));
- }, true);
- });
- }
- async function autoHideAll() {
- const obj = await getStoredObject(HIDDEN_POSTS_KEY);
- for (const boardUri in obj) {
- const arrSimple = obj[boardUri]?.simple || [];
- const arrPlus = obj[boardUri]?.plus || [];
- arrSimple.forEach(postId => setPostHidden(boardUri, String(postId), true, false));
- arrPlus.forEach(postId => setPostHidden(boardUri, String(postId), true, true));
- }
- const idsObj = await getStoredObject(FILTERED_IDS_KEY);
- for (const boardUri in idsObj) {
- for (const threadId in idsObj[boardUri]) {
- let threadObj = idsObj[boardUri][threadId];
- if (Array.isArray(threadObj)) {
- threadObj = { simple: threadObj, plus: [] };
- idsObj[boardUri][threadId] = threadObj;
- }
- (threadObj.simple || []).forEach(id => setPostsWithIdHidden(boardUri, threadId, id, true, false));
- (threadObj.plus || []).forEach(id => setPostsWithIdHidden(boardUri, threadId, id, true, true));
- }
- }
- let namesObj = await getStoredObject(FILTERED_NAMES_KEY);
- if (!namesObj || typeof namesObj !== "object" || Array.isArray(namesObj)) {
- namesObj = { simple: Array.isArray(namesObj) ? namesObj : [], plus: [] };
- }
- (namesObj.simple || []).forEach(name => setPostsWithNameHidden(name, true, false));
- (namesObj.plus || []).forEach(name => setPostsWithNameHidden(name, true, true));
- updateAllQuoteLinksFiltered();
- }
- const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: false });
- if (divPostsObs) {
- divPostsObs.addHandler(async function customPostHideMenuHandler(mutations) {
- const hiddenPostsObj = await getStoredObject(HIDDEN_POSTS_KEY);
- const filteredIdsObj = await getStoredObject(FILTERED_IDS_KEY);
- let filteredNamesObj = await getStoredObject(FILTERED_NAMES_KEY);
- if (!filteredNamesObj || typeof filteredNamesObj !== "object" || Array.isArray(filteredNamesObj)) {
- filteredNamesObj = { simple: Array.isArray(filteredNamesObj) ? filteredNamesObj : [], plus: [] };
- }
- if (!Array.isArray(filteredNamesObj.simple)) filteredNamesObj.simple = [];
- if (!Array.isArray(filteredNamesObj.plus)) filteredNamesObj.plus = [];
- const plusHiddenMap = {};
- for (const boardUri in hiddenPostsObj) {
- plusHiddenMap[boardUri] = new Set((hiddenPostsObj[boardUri]?.plus || []).map(String));
- }
- const plusFilteredIdPostIds = {};
- for (const boardUri in filteredIdsObj) {
- for (const threadId in filteredIdsObj[boardUri]) {
- let threadObj = filteredIdsObj[boardUri][threadId];
- if (Array.isArray(threadObj)) {
- threadObj = { simple: threadObj, plus: [] };
- filteredIdsObj[boardUri][threadId] = threadObj;
- }
- if (!plusFilteredIdPostIds[boardUri]) plusFilteredIdPostIds[boardUri] = {};
- plusFilteredIdPostIds[boardUri][threadId] = new Set();
- for (const id of threadObj.plus || []) {
- document.querySelectorAll(`.postCell[data-boarduri="${boardUri}"], .opCell[data-boarduri="${boardUri}"]`).forEach(cell => {
- const inner = getInnerPostElem(cell);
- const cellThreadId = getThreadIdFromInnerPost(inner);
- const idElem = cell.querySelector('.labelId');
- const cellId = idElem ? idElem.textContent.split(/[|\(]/)[0].trim() : null;
- if (
- cellThreadId === threadId &&
- cellId &&
- cellId === id
- ) {
- plusFilteredIdPostIds[boardUri][threadId].add(getPostId(cell));
- }
- });
- }
- }
- }
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- if (node.nodeType !== 1) continue;
- let postCells = [];
- if (node.classList && (node.classList.contains('postCell') || node.classList.contains('opCell'))) {
- postCells.push(node);
- }
- if (node.querySelectorAll) {
- postCells = postCells.concat(Array.from(node.querySelectorAll('.postCell, .opCell')));
- }
- for (const cell of postCells) {
- hijackHideButtons(cell);
- const boardUri = getBoardUri(cell);
- const postId = getPostId(cell);
- const inner = getInnerPostElem(cell);
- const threadId = getThreadIdFromInnerPost(inner);
- const idElem = cell.querySelector('.labelId');
- const id = idElem ? idElem.textContent.split(/[|\(]/)[0].trim() : null;
- const nameElem = cell.querySelector('.linkName');
- const name = nameElem ? nameElem.textContent.trim() : null;
- if (hiddenPostsObj[boardUri]?.simple?.includes(Number(postId))) {
- setPostHidden(boardUri, postId, true, false);
- }
- if (hiddenPostsObj[boardUri]?.plus?.includes(Number(postId))) {
- setPostHidden(boardUri, postId, true, true);
- }
- let threadObj = filteredIdsObj[boardUri] && filteredIdsObj[boardUri][threadId];
- if (Array.isArray(threadObj)) {
- threadObj = { simple: threadObj, plus: [] };
- filteredIdsObj[boardUri][threadId] = threadObj;
- } else if (!threadObj) {
- threadObj = { simple: [], plus: [] };
- if (!filteredIdsObj[boardUri]) filteredIdsObj[boardUri] = {};
- filteredIdsObj[boardUri][threadId] = threadObj;
- }
- if (id && threadObj.simple.includes(id)) {
- setPostsWithIdHidden(boardUri, threadId, id, true, false);
- }
- if (id && threadObj.plus.includes(id)) {
- setPostsWithIdHidden(boardUri, threadId, id, true, true);
- }
- if (name && filteredNamesObj.simple.includes(name)) {
- setPostsWithNameHidden(name, true, false);
- }
- if (name && filteredNamesObj.plus.includes(name)) {
- setPostsWithNameHidden(name, true, true);
- }
- let shouldHidePlus = false;
- const quoteLinks = cell.querySelectorAll('.quoteLink[data-target-uri]');
- if (!window._8chanSS_postParentMapCache) {
- const postParentMap = {};
- document.querySelectorAll('.postCell, .opCell').forEach(postCell => {
- const pid = postCell.id;
- const parentIds = [];
- postCell.querySelectorAll('.quoteLink[data-target-uri]').forEach(link => {
- const targetUri = link.getAttribute('data-target-uri');
- const match = targetUri && targetUri.match(/^([^#]+)#(\d+)$/);
- if (match) parentIds.push(match[2]);
- });
- postParentMap[pid] = parentIds;
- });
- window._8chanSS_postParentMapCache = postParentMap;
- }
- const postParentMap = window._8chanSS_postParentMapCache;
- if (!window._8chanSS_plusHiddenSetCache) {
- const plusHiddenSet = new Set();
- for (const b in plusHiddenMap) {
- for (const hid of plusHiddenMap[b]) {
- plusHiddenSet.add(hid);
- }
- }
- window._8chanSS_plusHiddenSetCache = plusHiddenSet;
- }
- const plusHiddenSet = window._8chanSS_plusHiddenSetCache;
- if (!window._8chanSS_filteredNamePlusSetCache) {
- const filteredNamePlusSet = new Set();
- const initialFiltered = [];
- document.querySelectorAll('.postCell, .opCell').forEach(postCell => {
- const nameElem = postCell.querySelector('.linkName');
- const name = nameElem ? nameElem.textContent.trim() : null;
- if (name && filteredNamesObj.plus.includes(name)) {
- filteredNamePlusSet.add(postCell.id);
- initialFiltered.push(postCell.id);
- }
- });
- const postParentMap = window._8chanSS_postParentMapCache;
- const childMap = {};
- Object.entries(postParentMap).forEach(([childId, parentIds]) => {
- parentIds.forEach(parentId => {
- if (!childMap[parentId]) childMap[parentId] = [];
- childMap[parentId].push(childId);
- });
- });
- const queue = [...initialFiltered];
- while (queue.length > 0) {
- const current = queue.shift();
- const children = childMap[current] || [];
- for (const child of children) {
- if (!filteredNamePlusSet.has(child)) {
- filteredNamePlusSet.add(child);
- queue.push(child);
- }
- }
- }
- window._8chanSS_filteredNamePlusSetCache = filteredNamePlusSet;
- }
- const filteredNamePlusSet = window._8chanSS_filteredNamePlusSetCache;
- const visited = new Set();
- function isDescendantOfPlusHiddenOrFilteredNamePlus(pid) {
- if (visited.has(pid)) return false;
- visited.add(pid);
- if (plusHiddenSet.has(pid)) return true;
- if (filteredNamePlusSet.has(pid)) return true;
- const parents = postParentMap[pid] || [];
- for (const par of parents) {
- if (isDescendantOfPlusHiddenOrFilteredNamePlus(par)) return true;
- }
- return false;
- }
- if (isDescendantOfPlusHiddenOrFilteredNamePlus(postId)) {
- shouldHidePlus = true;
- }
- if (shouldHidePlus) {
- setPostHidden(boardUri, postId, true, true);
- }
- if (
- plusFilteredIdPostIds[boardUri] &&
- plusFilteredIdPostIds[boardUri][threadId] &&
- plusFilteredIdPostIds[boardUri][threadId].size > 0
- ) {
- const panelBacklinks = cell.querySelector('.panelBacklinks');
- let isDirectReplyToFilteredId = false;
- if (panelBacklinks) {
- const backLinks = panelBacklinks.querySelectorAll('.backLink[data-target-uri]');
- for (const link of backLinks) {
- const targetUri = link.getAttribute('data-target-uri');
- const match = targetUri && targetUri.match(/^([^#]+)#(\d+)$/);
- if (match && plusFilteredIdPostIds[boardUri][threadId].has(match[2])) {
- isDirectReplyToFilteredId = true;
- break;
- }
- }
- }
- if (!isDirectReplyToFilteredId) {
- for (const link of quoteLinks) {
- const targetUri = link.getAttribute('data-target-uri');
- const match = targetUri && targetUri.match(/^([^#]+)#(\d+)$/);
- if (match && plusFilteredIdPostIds[boardUri][threadId].has(match[2])) {
- isDirectReplyToFilteredId = true;
- break;
- }
- }
- }
- if (isDirectReplyToFilteredId) {
- hidePostCellWithStub(cell, boardUri, postId, null, 'filteredIDPlus');
- }
- }
- }
- }
- }
- updateAllQuoteLinksFiltered();
- });
- }
- hijackHideButtons();
- autoHideAll();
- }
- async function enableIdFiltering() {
- if (!window.pageType?.isThread) return;
- const postCellSelector = ".postCell, .opCell, .innerOP";
- const labelIdSelector = ".labelId";
- const hiddenClassName = "is-hidden-by-filter";
- let activeFilterColor = null;
- const showIdLinksOnly = await getSetting("enableIdFilters_showIdLinksOnly");
- const showIdLinksVertical = await getSetting("enableIdFilters_showIdLinksVertical");
- let floatingDiv = null;
- function closeFloatingDiv() {
- if (floatingDiv && floatingDiv.parentNode) {
- floatingDiv.parentNode.removeChild(floatingDiv);
- floatingDiv = null;
- }
- document.removeEventListener("mousedown", outsideClickHandler, true);
- }
- function outsideClickHandler(e) {
- if (floatingDiv && !floatingDiv.contains(e.target)) {
- closeFloatingDiv();
- }
- }
- function showIdList(id, clickedLabel) {
- const idToMatch = (id.match(/^[a-fA-F0-9]{6}/) || [id.trim()])[0];
- const threadsContainer = document.getElementById('divThreads');
- if (!threadsContainer) {
- return [];
- }
- const allPosts = Array.from(threadsContainer.querySelectorAll('.postCell, .opCell, .innerOP'));
- const matchingPosts = [];
- allPosts.forEach(postEl => {
- const label = postEl.querySelector('.labelId');
- const postId = postEl.id;
- if (label && postId) {
- const labelId = (label.textContent.match(/^[a-fA-F0-9]{6}/) || [label.textContent.trim()])[0];
- if (labelId === idToMatch) {
- matchingPosts.push(postEl);
- }
- }
- });
- document.querySelectorAll('.ss-idlinks-floating').forEach(el => el.remove());
- const match = window.location.pathname.match(/^\/([^/]+)\/(res|last)\/(\d+)\.html/);
- const board = match ? match[1] : '';
- const thread = match ? match[3] : '';
- const floatingDiv = document.createElement('div');
- floatingDiv.className = 'ss-idlinks-floating';
- const title = document.createElement('div');
- title.style.fontWeight = 'bold';
- title.style.marginBottom = '8px';
- floatingDiv.appendChild(title);
- const linkContainer = document.createElement('div');
- if (!showIdLinksVertical) {
- title.textContent = `Posts by ID: ${idToMatch} (${matchingPosts.length})`;
- }
- linkContainer.style.display = showIdLinksVertical ? 'block' : 'flex';
- linkContainer.style.flexWrap = 'wrap';
- linkContainer.style.gap = '0.3em';
- matchingPosts.forEach(postEl => {
- const postId = postEl.id;
- const link = document.createElement('a');
- link.className = 'quoteLink postLink';
- link.href = `/${board}/res/${thread}.html#${postId}`;
- link.textContent = `>>${postId}`;
- link.setAttribute('data-target-uri', `${board}/${thread}#${postId}`);
- link.style.display = showIdLinksVertical ? 'block' : 'inline-block';
- link.onclick = function (e) {
- e.preventDefault();
- floatingDiv.remove();
- const target = document.getElementById(postId);
- if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' });
- };
- const wrapper = document.createElement('div');
- wrapper.className = 'innerPost';
- wrapper.dataset.uri = `${board}/${thread}#${postId}`;
- wrapper.appendChild(link);
- if(showIdLinksVertical){
- wrapper.style.boxShadow = 'none';
- wrapper.style.border = 'none';
- wrapper.style.outline = 'none';
- wrapper.style.backgroundColor = 'inherit';
- wrapper.style.display = 'block';
- wrapper.style.padding = 0;
- wrapper.style.margin = 0;
- }
- linkContainer.appendChild(wrapper);
- });
- floatingDiv.appendChild(linkContainer);
- document.body.appendChild(floatingDiv);
- const rect = clickedLabel.getBoundingClientRect();
- let top = rect.bottom + window.scrollY + 4;
- let left = rect.left + window.scrollX;
- if (left + 320 > window.innerWidth) left = Math.max(0, window.innerWidth - 340);
- if (top + 200 > window.innerHeight + window.scrollY) top = Math.max(10, rect.top + window.scrollY - 220);
- floatingDiv.style.top = `${top}px`;
- floatingDiv.style.left = `${left}px`;
- setTimeout(() => {
- function closeOnClick(e) {
- if (!floatingDiv.contains(e.target)) {
- floatingDiv.remove();
- document.removeEventListener('mousedown', closeOnClick, true);
- }
- }
- document.addEventListener('mousedown', closeOnClick, true);
- }, 0);
- return matchingPosts;
- }
- function applyFilter(targetRgbColor) {
- activeFilterColor = targetRgbColor;
- document.querySelectorAll(postCellSelector).forEach(cell => {
- const label = cell.querySelector(labelIdSelector);
- const matches = label && window.getComputedStyle(label).backgroundColor === targetRgbColor;
- cell.classList.toggle(hiddenClassName, !!targetRgbColor && !matches);
- });
- }
- function handleClick(event) {
- const clickedLabel = event.target.closest(labelIdSelector);
- if (clickedLabel && clickedLabel.closest(postCellSelector) && !clickedLabel.closest(".de-pview")) {
- event.preventDefault();
- event.stopPropagation();
- const id = clickedLabel.textContent.trim();
- if (showIdLinksOnly) {
- showIdList(id, clickedLabel);
- } else {
- const clickedColor = window.getComputedStyle(clickedLabel).backgroundColor;
- const rect = clickedLabel.getBoundingClientRect();
- const cursorOffsetY = event.clientY - rect.top;
- if (activeFilterColor === clickedColor) {
- applyFilter(null);
- } else {
- applyFilter(clickedColor);
- }
- clickedLabel.scrollIntoView({ behavior: "instant", block: "center" });
- window.scrollBy(0, cursorOffsetY - rect.height / 2);
- }
- }
- }
- document.body.addEventListener("click", handleClick);
- }
- async function createSettingsMenu() {
- let menu = document.getElementById("8chanSS-menu");
- if (menu) return menu;
- menu = document.createElement("div");
- menu.id = "8chanSS-menu";
- menu.style.position = "fixed";
- menu.style.top = "3rem";
- menu.style.left = "20rem";
- menu.style.zIndex = "99999";
- menu.style.background = "var(--menu-color)";
- menu.style.color = "var(--text-color)";
- menu.style.borderColor = "1px solid var(--border-color)";
- menu.style.padding = "0";
- menu.style.boxShadow = "0 4px 16px rgba(0,0,0,0.25)";
- menu.style.display = "none";
- menu.style.minWidth = "220px";
- menu.style.width = "100%";
- menu.style.maxWidth = "470px";
- menu.style.fontFamily = "sans-serif";
- menu.style.userSelect = "none";
- let isDragging = false,
- dragOffsetX = 0,
- dragOffsetY = 0;
- const header = document.createElement("div");
- header.style.display = "flex";
- header.style.justifyContent = "space-between";
- header.style.alignItems = "center";
- header.style.marginBottom = "0";
- header.style.cursor = "move";
- header.style.color = "var(--subject-color)";
- header.style.background = "var(--contrast-color)";
- header.style.padding = "1px 18px 1px";
- header.addEventListener("mousedown", function (e) {
- isDragging = true;
- const rect = menu.getBoundingClientRect();
- dragOffsetX = e.clientX - rect.left;
- dragOffsetY = e.clientY - rect.top;
- document.body.style.userSelect = "none";
- });
- document.addEventListener("mousemove", function (e) {
- if (!isDragging) return;
- let newLeft = e.clientX - dragOffsetX;
- let newTop = e.clientY - dragOffsetY;
- const menuRect = menu.getBoundingClientRect();
- const menuWidth = menuRect.width;
- const menuHeight = menuRect.height;
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
- newLeft = Math.max(0, Math.min(newLeft, viewportWidth - menuWidth));
- newTop = Math.max(0, Math.min(newTop, viewportHeight - menuHeight));
- menu.style.left = newLeft + "px";
- menu.style.top = newTop + "px";
- menu.style.right = "auto";
- });
- document.addEventListener("mouseup", function () {
- isDragging = false;
- document.body.style.userSelect = "";
- });
- const title = document.createElement("span");
- title.textContent = "8chanSS Settings";
- title.style.fontWeight = "bold";
- header.appendChild(title);
- const closeBtn = document.createElement("button");
- closeBtn.textContent = "✕";
- closeBtn.style.background = "none";
- closeBtn.style.setProperty("background", "none", "important");
- closeBtn.style.border = "none";
- closeBtn.style.color = "var(--subject-color)";
- closeBtn.style.fontSize = "18px";
- closeBtn.style.cursor = "pointer";
- closeBtn.style.marginLeft = "10px";
- closeBtn.addEventListener("click", () => {
- menu.style.display = "none";
- });
- header.appendChild(closeBtn);
- menu.appendChild(header);
- const closeOnOutsideClick = (e) => {
- if (menu.style.display !== "none" && !menu.contains(e.target)) {
- const menuToggle = document.getElementById("8chanSS-icon");
- if (menuToggle && !menuToggle.contains(e.target)) {
- menu.style.display = "none";
- }
- }
- };
- Object.defineProperty(menu.style, 'display', {
- set: function (value) {
- const oldValue = this.getPropertyValue('display');
- this.setProperty('display', value);
- if (oldValue === 'none' && value !== 'none') {
- setTimeout(() => {
- document.addEventListener('click', closeOnOutsideClick);
- }, 10);
- }
- else if (oldValue !== 'none' && value === 'none') {
- document.removeEventListener('click', closeOnOutsideClick);
- }
- },
- get: function () {
- return this.getPropertyValue('display');
- }
- });
- const tabNav = document.createElement("div");
- tabNav.style.display = "flex";
- tabNav.style.borderBottom = "1px solid #444";
- tabNav.style.background = "rgb(from var(--menu-color) r g b / 1)";
- const tabContent = document.createElement("div");
- tabContent.style.padding = "15px 16px";
- tabContent.style.maxHeight = "70vh";
- tabContent.style.overflowY = "auto";
- tabContent.style.scrollbarWidth = "thin";
- tabContent.style.fontSize = "smaller";
- const tempSettings = {};
- await Promise.all(
- Object.keys(flatSettings).map(async (key) => {
- tempSettings[key] = await getSetting(key);
- })
- );
- const tabs = {
- site: {
- label: "Site",
- content: createTabContent("site", tempSettings),
- },
- threads: {
- label: "Threads",
- content: createTabContent("threads", tempSettings),
- },
- catalog: {
- label: "Catalog",
- content: createTabContent("catalog", tempSettings),
- },
- styling: {
- label: "Style",
- content: createTabContent("styling", tempSettings),
- },
- miscel: {
- label: "Misc.",
- content: createTabContent("miscel", tempSettings),
- },
- shortcuts: {
- label: "⌨️",
- content: createShortcutsTab(),
- },
- };
- Object.keys(tabs).forEach((tabId, index, arr) => {
- const tab = tabs[tabId];
- const tabButton = document.createElement("button");
- tabButton.textContent = tab.label;
- tabButton.dataset.tab = tabId;
- tabButton.style.background = index === 0 ? "var(--contrast-color)" : "transparent";
- tabButton.style.border = "none";
- tabButton.style.borderRight = "1px solid #444";
- tabButton.style.setProperty("border-left-radius", "0", "important");
- tabButton.style.color = "var(--text-color)";
- tabButton.style.padding = "8px 15px";
- tabButton.style.margin = "5px 0 0 0";
- tabButton.style.setProperty("border-top-right-radius", "0", "important");
- tabButton.style.setProperty("border-bottom-right-radius", "0", "important");
- tabButton.style.cursor = "pointer";
- tabButton.style.flex = "1";
- tabButton.style.fontSize = "14px";
- tabButton.style.transition = "background 0.2s";
- if (index === 0) {
- tabButton.style.setProperty("border-top-left-radius", "8px", "important");
- tabButton.style.setProperty("border-top-right-radius", "0", "important");
- tabButton.style.setProperty("border-bottom-left-radius", "0", "important");
- tabButton.style.setProperty("border-bottom-right-radius", "0", "important");
- tabButton.style.margin = "5px 0 0 5px";
- }
- if (index === arr.length - 1) {
- tabButton.style.setProperty("border-top-right-radius", "8px", "important");
- tabButton.style.setProperty("border-top-left-radius", "0", "important");
- tabButton.style.setProperty("border-bottom-left-radius", "0", "important");
- tabButton.style.setProperty("border-bottom-right-radius", "0", "important");
- tabButton.style.margin = "5px 5px 0 0";
- tabButton.style.borderRight = "none";
- }
- tabButton.addEventListener("click", () => {
- Object.values(tabs).forEach((t) => {
- t.content.style.display = "none";
- });
- tab.content.style.display = "block";
- tabNav.querySelectorAll("button").forEach((btn) => {
- btn.style.background = "transparent";
- });
- tabButton.style.background = "var(--contrast-color)";
- });
- tabNav.appendChild(tabButton);
- });
- menu.appendChild(tabNav);
- Object.values(tabs).forEach((tab, index) => {
- tab.content.style.display = index === 0 ? "block" : "none";
- tabContent.appendChild(tab.content);
- });
- menu.appendChild(tabContent);
- const buttonContainer = document.createElement("div");
- buttonContainer.style.display = "flex";
- buttonContainer.style.gap = "10px";
- buttonContainer.style.padding = "0 18px 15px";
- const saveBtn = document.createElement("button");
- saveBtn.textContent = "Save";
- saveBtn.style.setProperty("background", "#4caf50", "important");
- saveBtn.style.setProperty("color", "#fff", "important");
- saveBtn.style.border = "none";
- saveBtn.style.borderRadius = "4px";
- saveBtn.style.padding = "8px 18px";
- saveBtn.style.fontSize = "15px";
- saveBtn.style.cursor = "pointer";
- saveBtn.style.flex = "1";
- saveBtn.addEventListener("click", async function () {
- for (const key of Object.keys(tempSettings)) {
- await setSetting(key, tempSettings[key]);
- }
- saveBtn.textContent = "Saved!";
- setTimeout(() => {
- saveBtn.textContent = "Save";
- }, 900);
- setTimeout(() => {
- window.location.reload();
- }, 400);
- });
- buttonContainer.appendChild(saveBtn);
- const resetBtn = document.createElement("button");
- resetBtn.textContent = "Reset";
- resetBtn.style.setProperty("background", "#dd3333", "important");
- resetBtn.style.setProperty("color", "#fff", "important");
- resetBtn.style.border = "none";
- resetBtn.style.borderRadius = "4px";
- resetBtn.style.padding = "8px 18px";
- resetBtn.style.fontSize = "15px";
- resetBtn.style.cursor = "pointer";
- resetBtn.style.flex = "1";
- resetBtn.addEventListener("click", async function () {
- if (confirm("Reset all 8chanSS settings to defaults?")) {
- const keys = await GM.listValues();
- for (const key of keys) {
- if (key.startsWith("8chanSS_")) {
- await GM.deleteValue(key);
- }
- }
- resetBtn.textContent = "Reset!";
- setTimeout(() => {
- resetBtn.textContent = "Reset";
- }, 900);
- setTimeout(() => {
- window.location.reload();
- }, 400);
- }
- });
- buttonContainer.appendChild(resetBtn);
- menu.appendChild(buttonContainer);
- const info = document.createElement("div");
- info.style.fontSize = "11px";
- info.style.padding = "0 18px 12px";
- info.style.opacity = "0.7";
- info.style.textAlign = "center";
- 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>';
- menu.appendChild(info);
- document.body.appendChild(menu);
- return menu;
- }
- function createTabContent(category, tempSettings) {
- const container = document.createElement("div");
- const categorySettings = scriptSettings[category];
- Object.keys(categorySettings).forEach((key) => {
- const setting = categorySettings[key];
- if (setting.type === "separator") {
- const hr = document.createElement("hr");
- hr.style.border = "none";
- hr.style.borderTop = "1px solid #444";
- hr.style.margin = "12px 0";
- container.appendChild(hr);
- return;
- }
- if (setting.type === "title") {
- const title = document.createElement("div");
- title.textContent = setting.label;
- title.style.fontWeight = "bold";
- title.style.color = "var(--subject-title)";
- title.style.fontSize = "1rem";
- title.style.margin = "10px 0 6px 0";
- title.style.opacity = "0.9";
- container.appendChild(title);
- return;
- }
- const parentRow = document.createElement("div");
- parentRow.style.display = "flex";
- parentRow.style.alignItems = "center";
- parentRow.style.marginBottom = "0px";
- if (key === "hoverVideoVolume" && setting.type === "number") {
- const label = document.createElement("label");
- label.htmlFor = "setting_" + key;
- label.textContent = setting.label + ": ";
- label.style.flex = "1";
- const sliderContainer = document.createElement("div");
- sliderContainer.style.display = "flex";
- sliderContainer.style.alignItems = "center";
- sliderContainer.style.flex = "1";
- const slider = document.createElement("input");
- slider.type = "range";
- slider.id = "setting_" + key;
- slider.min = setting.min;
- slider.max = setting.max;
- slider.value = Number(tempSettings[key]).toString();
- slider.style.flex = "unset";
- slider.style.width = "100px";
- slider.style.marginRight = "10px";
- const valueLabel = document.createElement("span");
- valueLabel.textContent = slider.value + "%";
- valueLabel.style.minWidth = "40px";
- valueLabel.style.textAlign = "right";
- slider.addEventListener("input", function () {
- let val = Number(slider.value);
- if (isNaN(val)) val = setting.default;
- val = Math.max(setting.min, Math.min(setting.max, val));
- slider.value = val.toString();
- tempSettings[key] = val;
- valueLabel.textContent = val + "%";
- });
- sliderContainer.appendChild(slider);
- sliderContainer.appendChild(valueLabel);
- parentRow.appendChild(label);
- parentRow.appendChild(sliderContainer);
- const wrapper = document.createElement("div");
- wrapper.style.marginBottom = "10px";
- wrapper.appendChild(parentRow);
- container.appendChild(wrapper);
- return;
- }
- const checkbox = document.createElement("input");
- checkbox.type = "checkbox";
- checkbox.id = "setting_" + key;
- checkbox.checked =
- tempSettings[key] === true || tempSettings[key] === "true";
- checkbox.style.marginRight = "8px";
- const label = document.createElement("label");
- label.htmlFor = checkbox.id;
- label.textContent = setting.label;
- label.style.flex = "1";
- let chevron = null;
- let subOptionsContainer = null;
- if (setting?.subOptions) {
- chevron = document.createElement("span");
- chevron.className = "ss-chevron";
- chevron.innerHTML = "▶";
- chevron.style.display = "inline-block";
- chevron.style.transition = "transform 0.2s";
- chevron.style.marginLeft = "6px";
- chevron.style.fontSize = "12px";
- chevron.style.userSelect = "none";
- chevron.style.transform = checkbox.checked
- ? "rotate(90deg)"
- : "rotate(0deg)";
- }
- checkbox.addEventListener("change", function () {
- tempSettings[key] = checkbox.checked;
- if (!setting?.subOptions) return;
- if (!subOptionsContainer) return;
- subOptionsContainer.style.display = checkbox.checked
- ? "block"
- : "none";
- if (!chevron) return;
- chevron.style.transform = checkbox.checked
- ? "rotate(90deg)"
- : "rotate(0deg)";
- });
- parentRow.appendChild(checkbox);
- parentRow.appendChild(label);
- if (chevron) parentRow.appendChild(chevron);
- const wrapper = document.createElement("div");
- wrapper.style.marginBottom = "10px";
- wrapper.appendChild(parentRow);
- if (setting?.subOptions) {
- subOptionsContainer = document.createElement("div");
- subOptionsContainer.style.marginLeft = "25px";
- subOptionsContainer.style.marginTop = "5px";
- subOptionsContainer.style.display = checkbox.checked ? "block" : "none";
- Object.keys(setting.subOptions).forEach((subKey) => {
- const subSetting = setting.subOptions[subKey];
- const fullKey = `${key}_${subKey}`;
- const subWrapper = document.createElement("div");
- subWrapper.style.marginBottom = "5px";
- if (subSetting.type === "text") {
- const subLabel = document.createElement("label");
- subLabel.htmlFor = "setting_" + fullKey;
- subLabel.textContent = subSetting.label + ": ";
- const subInput = document.createElement("input");
- subInput.type = "text";
- subInput.id = "setting_" + fullKey;
- subInput.value = tempSettings[fullKey] || "";
- subInput.maxLength = subSetting.maxLength;
- subInput.style.width = "60px";
- subInput.style.marginLeft = "2px";
- subInput.placeholder = "(!) ";
- subInput.addEventListener("input", function () {
- let val = subInput.value.replace(/[<>"']/g, "");
- if (val.length > subInput.maxLength) {
- val = val.slice(0, subInput.maxLength);
- }
- subInput.value = val;
- tempSettings[fullKey] = val;
- });
- subWrapper.appendChild(subLabel);
- subWrapper.appendChild(subInput);
- } else if (subSetting.type === "textarea") {
- const subLabel = document.createElement("label");
- subLabel.htmlFor = "setting_" + fullKey;
- subLabel.textContent = subSetting.label + ": ";
- const subTextarea = document.createElement("textarea");
- subTextarea.id = "setting_" + fullKey;
- subTextarea.value = tempSettings[fullKey] || "";
- subTextarea.rows = subSetting.rows || 4;
- subTextarea.style.width = "90%";
- subTextarea.style.margin = "5px 0 0";
- subTextarea.placeholder = subSetting.placeholder || "";
- subTextarea.addEventListener("input", function () {
- tempSettings[fullKey] = subTextarea.value;
- });
- subWrapper.appendChild(subLabel);
- subWrapper.appendChild(document.createElement("br"));
- subWrapper.appendChild(subTextarea);
- } else if (subSetting.type === "number") {
- const subLabel = document.createElement("label");
- subLabel.htmlFor = "setting_" + fullKey;
- subLabel.textContent = subSetting.label + ": ";
- const subInput = document.createElement("input");
- subInput.type = "number";
- subInput.id = "setting_" + fullKey;
- subInput.value = tempSettings[fullKey] || subSetting.default;
- if (subSetting.min !== undefined) subInput.min = subSetting.min;
- if (subSetting.max !== undefined) subInput.max = subSetting.max;
- subInput.style.width = "60px";
- subInput.style.marginLeft = "2px";
- subInput.addEventListener("input", function () {
- let val = Number(subInput.value);
- if (isNaN(val)) val = subSetting.default;
- if (subSetting.min !== undefined) val = Math.max(subSetting.min, val);
- if (subSetting.max !== undefined) val = Math.min(subSetting.max, val);
- subInput.value = val;
- tempSettings[fullKey] = val;
- });
- subWrapper.appendChild(subLabel);
- subWrapper.appendChild(subInput);
- } else if (subSetting.type === "select") {
- const subSelect = document.createElement("select");
- subSelect.id = "setting_" + fullKey;
- subSelect.style.marginLeft = "5px";
- subSelect.style.width = "120px";
- if (Array.isArray(subSetting.options)) {
- subSetting.options.forEach(option => {
- const optionEl = document.createElement("option");
- optionEl.value = option.value;
- optionEl.textContent = option.label;
- if (tempSettings[fullKey] === option.value) {
- optionEl.selected = true;
- }
- subSelect.appendChild(optionEl);
- });
- }
- if (!subSelect.value && subSetting.default) {
- subSelect.value = subSetting.default;
- tempSettings[fullKey] = subSetting.default;
- }
- subSelect.addEventListener("change", function () {
- tempSettings[fullKey] = subSelect.value;
- if (key === "customFavicon" && tempSettings["customFavicon"]) {
- faviconManager.setFaviconStyle(subSelect.value, "base");
- }
- if (key === "faviconStyle" && tempSettings["customFavicon"]) {
- faviconManager.setFaviconStyle(subSelect.value, "base");
- }
- });
- const subLabel = document.createElement("label");
- subLabel.htmlFor = "setting_" + fullKey;
- subLabel.textContent = subSetting.label || fullKey;
- subLabel.style.marginLeft = "10px";
- subWrapper.appendChild(subLabel);
- subWrapper.appendChild(subSelect);
- } else {
- const subCheckbox = document.createElement("input");
- subCheckbox.type = "checkbox";
- subCheckbox.id = "setting_" + fullKey;
- subCheckbox.checked = tempSettings[fullKey];
- subCheckbox.style.marginRight = "8px";
- subCheckbox.addEventListener("change", function () {
- tempSettings[fullKey] = subCheckbox.checked;
- });
- const subLabel = document.createElement("label");
- subLabel.htmlFor = subCheckbox.id;
- subLabel.textContent = subSetting.label;
- subWrapper.appendChild(subCheckbox);
- subWrapper.appendChild(subLabel);
- }
- subOptionsContainer.appendChild(subWrapper);
- });
- wrapper.appendChild(subOptionsContainer);
- }
- container.appendChild(wrapper);
- });
- return container;
- }
- const themeSelector = document.getElementById("themesBefore");
- let link = null;
- let bracketSpan = null;
- if (themeSelector) {
- bracketSpan = document.createElement("span");
- bracketSpan.textContent = "] [ ";
- link = document.createElement("a");
- link.id = "8chanSS-icon";
- link.href = "#";
- link.textContent = "8chanSS";
- link.style.fontWeight = "bold";
- themeSelector.parentNode.insertBefore(
- bracketSpan,
- themeSelector.nextSibling
- );
- themeSelector.parentNode.insertBefore(link, bracketSpan.nextSibling);
- }
- function createShortcutsTab() {
- const container = document.createElement("div");
- const title = document.createElement("h3");
- title.textContent = "Keyboard Shortcuts";
- title.style.margin = "0 0 15px 0";
- title.style.fontSize = "16px";
- container.appendChild(title);
- const table = document.createElement("table");
- table.style.width = "100%";
- table.style.borderCollapse = "collapse";
- const tableStyles = {
- th: {
- textAlign: "left",
- padding: "8px 5px",
- borderBottom: "1px solid #444",
- fontSize: "14px",
- fontWeight: "bold",
- },
- td: {
- padding: "8px 5px",
- borderBottom: "1px solid #333",
- fontSize: "13px",
- },
- kbd: {
- background: "#f7f7f7",
- color: "#000",
- border: "1px solid #555",
- borderRadius: "3px",
- padding: "2px 5px",
- fontSize: "12px",
- fontFamily: "monospace",
- },
- };
- const headerRow = document.createElement("tr");
- const shortcutHeader = document.createElement("th");
- shortcutHeader.textContent = "Shortcut";
- Object.assign(shortcutHeader.style, tableStyles.th);
- headerRow.appendChild(shortcutHeader);
- const actionHeader = document.createElement("th");
- actionHeader.textContent = "Action";
- Object.assign(actionHeader.style, tableStyles.th);
- headerRow.appendChild(actionHeader);
- table.appendChild(headerRow);
- const shortcuts = [
- { keys: ["Ctrl", "F1"], action: "Open 8chanSS settings" },
- { keys: ["Tab"], action: "Target Quick Reply text area" },
- { keys: ["R"], action: "Refresh Thread (5 sec. cooldown)" },
- { keys: ["Ctrl", "Q"], action: "Toggle Quick Reply" },
- { keys: ["Ctrl", "Enter"], action: "Submit post" },
- { keys: ["Escape"], action: "Clear QR textarea and hide all dialogs" },
- { keys: ["ALT", "W"], action: "Watch Thread" },
- { keys: ["SHIFT", "T"], action: "Toggle Quote Threading" },
- { keys: ["SHIFT", "M1"], action: "Hide Thread in Catalog" },
- { keys: ["CTRL", "UP/DOWN"], action: "Scroll between Your Replies" },
- { keys: ["CTRL", "SHIFT", "UP/DOWN"], action: "Scroll between Replies to You" },
- { keys: ["Ctrl", "B"], action: "Bold text" },
- { keys: ["Ctrl", "I"], action: "Italic text" },
- { keys: ["Ctrl", "U"], action: "Underline text" },
- { keys: ["Ctrl", "S"], action: "Spoiler text" },
- { keys: ["Ctrl", "D"], action: "Srz Bizniz text" },
- { keys: ["Ctrl", "M"], action: "Moe text" },
- { keys: ["Alt", "C"], action: "Code block" },
- ];
- shortcuts.forEach((shortcut) => {
- const row = document.createElement("tr");
- const shortcutCell = document.createElement("td");
- Object.assign(shortcutCell.style, tableStyles.td);
- shortcut.keys.forEach((key, index) => {
- const kbd = document.createElement("kbd");
- kbd.textContent = key;
- Object.assign(kbd.style, tableStyles.kbd);
- shortcutCell.appendChild(kbd);
- if (index < shortcut.keys.length - 1) {
- const plus = document.createTextNode(" + ");
- shortcutCell.appendChild(plus);
- }
- });
- row.appendChild(shortcutCell);
- const actionCell = document.createElement("td");
- actionCell.textContent = shortcut.action;
- Object.assign(actionCell.style, tableStyles.td);
- row.appendChild(actionCell);
- table.appendChild(row);
- });
- container.appendChild(table);
- const note = document.createElement("p");
- note.textContent =
- "Text formatting shortcuts work when text is selected or when inserting at cursor position.";
- note.style.fontSize = "12px";
- note.style.marginTop = "15px";
- note.style.opacity = "0.7";
- note.style.fontStyle = "italic";
- container.appendChild(note);
- return container;
- }
- if (link) {
- let menu = await createSettingsMenu();
- link.style.cursor = "pointer";
- link.title = "Open 8chanSS settings";
- link.addEventListener("click", async function (e) {
- e.preventDefault();
- let menu = await createSettingsMenu();
- menu.style.display = menu.style.display === "none" ? "block" : "none";
- });
- }
- async function shortcutsGloballyEnabled() {
- return await getSetting("enableShortcuts");
- }
- const bbCodeCombinations = new Map([
- ["s", ["[spoiler]", "[/spoiler]"]],
- ["b", ["'''", "'''"]],
- ["u", ["__", "__"]],
- ["i", ["''", "''"]],
- ["d", ["==", "=="]],
- ["m", ["[moe]", "[/moe]"]],
- ["c", ["[code]", "[/code]"]],
- ]);
- function applyBBCode(textBox, key) {
- const [openTag, closeTag] = bbCodeCombinations.get(key);
- const { selectionStart, selectionEnd, value } = textBox;
- if (selectionStart === selectionEnd) {
- const before = value.slice(0, selectionStart);
- const after = value.slice(selectionEnd);
- const newCursor = selectionStart + openTag.length;
- textBox.value = before + openTag + closeTag + after;
- textBox.selectionStart = textBox.selectionEnd = newCursor;
- } else {
- const before = value.slice(0, selectionStart);
- const selected = value.slice(selectionStart, selectionEnd);
- const after = value.slice(selectionEnd);
- textBox.value = before + openTag + selected + closeTag + after;
- textBox.selectionStart = selectionStart + openTag.length;
- textBox.selectionEnd = selectionEnd + openTag.length;
- }
- }
- let lastHighlighted = null;
- let lastType = null;
- let lastRefreshTime = 0;
- function getEligiblePostCells(isOwnReply) {
- const selector = isOwnReply
- ? '.postCell:has(a.youName), .opCell:has(a.youName)'
- : '.postCell:has(a.quoteLink.you), .opCell:has(a.quoteLink.you)';
- return Array.from(document.querySelectorAll(selector));
- }
- function scrollToReply(isOwnReply = true, getNextReply = true) {
- const postCells = getEligiblePostCells(isOwnReply);
- if (!postCells.length) return;
- let currentIndex = -1;
- const expectedType = isOwnReply ? "own" : "reply";
- if (
- lastType === expectedType &&
- lastHighlighted
- ) {
- const container = lastHighlighted.closest('.postCell, .opCell');
- currentIndex = postCells.indexOf(container);
- }
- if (currentIndex === -1) {
- const viewportMiddle = window.innerHeight / 2;
- currentIndex = postCells.findIndex(cell => {
- const rect = cell.getBoundingClientRect();
- return rect.top + rect.height / 2 > viewportMiddle;
- });
- if (currentIndex === -1) {
- currentIndex = getNextReply ? -1 : postCells.length;
- }
- }
- const targetIndex = getNextReply ? currentIndex + 1 : currentIndex - 1;
- if (targetIndex < 0 || targetIndex >= postCells.length) return;
- const postContainer = postCells[targetIndex];
- if (postContainer) {
- postContainer.scrollIntoView({ behavior: "smooth", block: "center" });
- if (lastHighlighted) {
- lastHighlighted.classList.remove('target-highlight');
- }
- let anchorId = null;
- let anchorElem = postContainer.querySelector('[id^="p"]');
- if (anchorElem && anchorElem.id) {
- anchorId = anchorElem.id;
- } else if (postContainer.id) {
- anchorId = postContainer.id;
- }
- if (anchorId && location.hash !== '#' + anchorId) {
- history.replaceState(null, '', '#' + anchorId);
- }
- const innerPost = postContainer.querySelector('.innerPost');
- if (innerPost) {
- innerPost.classList.add('target-highlight');
- lastHighlighted = innerPost;
- } else {
- lastHighlighted = null;
- }
- lastType = isOwnReply ? "own" : "reply";
- }
- }
- window.addEventListener('hashchange', () => {
- if (lastHighlighted) {
- lastHighlighted.classList.remove('target-highlight');
- lastHighlighted = null;
- }
- const hash = location.hash.replace('#', '');
- if (hash) {
- const postElem = document.getElementById(hash);
- if (postElem) {
- const innerPost = postElem.querySelector('.innerPost');
- if (innerPost) {
- innerPost.classList.add('target-highlight');
- lastHighlighted = innerPost;
- }
- }
- }
- });
- document.addEventListener("keydown", async function (event) {
- if (!(await shortcutsGloballyEnabled())) return;
- const active = document.activeElement;
- if (
- active &&
- event.key !== "Tab" &&
- (active.tagName === "INPUT" ||
- active.tagName === "TEXTAREA" ||
- active.isContentEditable)
- ) {
- return;
- }
- if (event.ctrlKey && event.key === "F1") {
- event.preventDefault();
- let menu = document.getElementById("8chanSS-menu") || (await createSettingsMenu());
- menu.style.display = menu.style.display === "none" || menu.style.display === "" ? "block" : "none";
- return;
- }
- if (event.ctrlKey && (event.key === "q" || event.key === "Q")) {
- event.preventDefault();
- const hiddenDiv = document.getElementById("quick-reply");
- if (!hiddenDiv) return;
- const isHidden = hiddenDiv.style.display === "none" || hiddenDiv.style.display === "";
- hiddenDiv.style.display = isHidden ? "block" : "none";
- if (isHidden) {
- setTimeout(() => {
- const textarea = document.getElementById("qrbody");
- if (textarea) textarea.focus();
- }, 50);
- }
- return;
- }
- if (
- event.key === "Tab" &&
- !event.ctrlKey &&
- !event.altKey &&
- !event.metaKey
- ) {
- const qrbody = document.getElementById("qrbody");
- const captcha = document.getElementById("QRfieldCaptcha");
- if (qrbody) {
- if (document.activeElement === qrbody && captcha) {
- event.preventDefault();
- captcha.focus();
- } else if (document.activeElement === captcha) {
- event.preventDefault();
- qrbody.focus();
- } else if (document.activeElement !== qrbody) {
- event.preventDefault();
- qrbody.focus();
- }
- }
- return;
- }
- if (event.key === "r" || event.key === "R") {
- const isThread = window.pageType?.isThread;
- const isCatalog = window.pageType?.isCatalog;
- const threadRefreshBtn = document.getElementById("refreshButton");
- const catalogRefreshBtn = document.getElementById("catalogRefreshButton");
- const now = Date.now();
- if (
- (isThread && threadRefreshBtn) ||
- (isCatalog && catalogRefreshBtn)
- ) {
- if (now - lastRefreshTime >= 5000) {
- event.preventDefault();
- if (isThread && threadRefreshBtn) {
- threadRefreshBtn.click();
- } else if (isCatalog && catalogRefreshBtn) {
- catalogRefreshBtn.click();
- }
- lastRefreshTime = now;
- } else {
- event.preventDefault();
- }
- return;
- }
- }
- if (event.shiftKey && !event.ctrlKey && !event.altKey && (event.key === "t" || event.key === "T")) {
- event.preventDefault();
- const current = await getSetting("quoteThreading");
- const newValue = !current;
- await setSetting("quoteThreading", newValue);
- try {
- const msg = `Quote threading <b>${newValue ? "enabled" : "disabled"}</b>`;
- const color = newValue ? 'blue' : 'black';
- callPageToast(msg, color, 1300);
- } catch { }
- setTimeout(() => window.location.reload(), 1400);
- return;
- }
- if (event.key === "Escape") {
- const textarea = document.getElementById("qrbody");
- if (textarea) textarea.value = "";
- const quickReply = document.getElementById("quick-reply");
- if (quickReply) quickReply.style.display = "none";
- const threadWatcher = document.getElementById("watchedMenu");
- if (threadWatcher) threadWatcher.style.display = "none";
- return;
- }
- if (event.ctrlKey && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
- event.preventDefault();
- const isOwnReply = !event.shiftKey;
- const isNext = event.key === 'ArrowDown';
- scrollToReply(isOwnReply, isNext);
- return;
- }
- if (
- event.altKey &&
- (event.key === "w" || event.key === "W")
- ) {
- event.preventDefault();
- const btn = document.querySelector(".watchButton");
- if (btn && !btn.classList.contains("watched-active")) {
- btn.click();
- setTimeout(() => {
- btn.classList.add("watched-active");
- }, 100);
- }
- return;
- }
- });
- const replyTextarea = document.getElementById("qrbody");
- if (!(await shortcutsGloballyEnabled())) {
- return;
- } else if (replyTextarea) {
- replyTextarea.addEventListener("keydown", async function (event) {
- if (event.ctrlKey && event.key === "Enter") {
- event.preventDefault();
- const submitButton = document.getElementById("qrbutton");
- if (submitButton) {
- submitButton.click();
- if (await getSetting("watchThreadOnReply")) {
- setTimeout(() => {
- const btn = document.querySelector(".watchButton");
- if (btn && !btn.classList.contains("watched-active")) {
- btn.click();
- setTimeout(() => {
- btn.classList.add("watched-active");
- }, 100);
- }
- }, 500);
- }
- }
- }
- });
- replyTextarea.addEventListener("keydown", function (event) {
- const key = event.key.toLowerCase();
- if (key === "c" && event.altKey && !event.ctrlKey && bbCodeCombinations.has(key)) {
- event.preventDefault();
- applyBBCode(event.target, key);
- return;
- }
- if (event.ctrlKey && !event.altKey && bbCodeCombinations.has(key) && key !== "c") {
- event.preventDefault();
- applyBBCode(event.target, key);
- return;
- }
- });
- }
- function featureCatalogHiding() {
- const STORAGE_KEY = "8chanSS_hiddenCatalogThreads";
- let showHiddenMode = false;
- function getBoardAndThreadNumFromCell(cell) {
- const link = cell.querySelector("a.linkThumb[href*='/res/']");
- if (!link) return { board: null, threadNum: null };
- const match = link.getAttribute("href").match(/^\/([^/]+)\/res\/(\d+)\.html/);
- if (!match) return { board: null, threadNum: null };
- return { board: match[1], threadNum: match[2] };
- }
- async function loadHiddenThreadsObj() {
- const raw = await GM.getValue(STORAGE_KEY, "{}");
- try {
- const obj = JSON.parse(raw);
- return typeof obj === "object" && obj !== null ? obj : {};
- } catch {
- return {};
- }
- }
- async function saveHiddenThreadsObj(obj) {
- await GM.setValue(STORAGE_KEY, JSON.stringify(obj));
- }
- async function applyHiddenThreads() {
- const hiddenThreadsObjRaw = await GM.getValue(STORAGE_KEY, "{}");
- let hiddenThreadsObj;
- try {
- hiddenThreadsObj = JSON.parse(hiddenThreadsObjRaw);
- if (typeof hiddenThreadsObj !== "object" || hiddenThreadsObj === null) hiddenThreadsObj = {};
- } catch {
- hiddenThreadsObj = {};
- }
- document.querySelectorAll(".catalogCell").forEach(cell => {
- const { board, threadNum } = getBoardAndThreadNumFromCell(cell);
- if (!board || !threadNum) return;
- const hiddenThreads = hiddenThreadsObj[board] || [];
- if (typeof showHiddenMode !== "undefined" && showHiddenMode) {
- if (hiddenThreads.includes(threadNum)) {
- cell.style.display = "";
- cell.classList.add("ss-unhide-thread");
- cell.classList.remove("ss-hidden-thread");
- } else {
- cell.style.display = "none";
- cell.classList.remove("ss-unhide-thread", "ss-hidden-thread");
- }
- } else {
- if (hiddenThreads.includes(threadNum)) {
- cell.style.display = "none";
- cell.classList.add("ss-hidden-thread");
- cell.classList.remove("ss-unhide-thread");
- } else {
- cell.style.display = "";
- cell.classList.remove("ss-hidden-thread", "ss-unhide-thread");
- }
- }
- });
- }
- async function onCatalogCellClick(e) {
- const cell = e.target.closest(".catalogCell");
- if (!cell) return;
- if (e.shiftKey && e.button === 0) {
- const { board, threadNum } = getBoardAndThreadNumFromCell(cell);
- if (!board || !threadNum) return;
- let hiddenThreadsObj = await loadHiddenThreadsObj();
- if (!hiddenThreadsObj[board]) hiddenThreadsObj[board] = [];
- let hiddenThreads = hiddenThreadsObj[board];
- if (showHiddenMode) {
- hiddenThreads = hiddenThreads.filter(num => num !== threadNum);
- hiddenThreadsObj[board] = hiddenThreads;
- await saveHiddenThreadsObj(hiddenThreadsObj);
- await applyHiddenThreads();
- } else {
- if (!hiddenThreads.includes(threadNum)) {
- hiddenThreads.push(threadNum);
- hiddenThreadsObj[board] = hiddenThreads;
- }
- await saveHiddenThreadsObj(hiddenThreadsObj);
- cell.style.display = "none";
- cell.classList.add("ss-hidden-thread");
- }
- e.preventDefault();
- e.stopPropagation();
- }
- }
- async function showAllHiddenThreads() {
- showHiddenMode = true;
- await applyHiddenThreads();
- const btn = document.getElementById("ss-show-hidden-btn");
- if (btn) btn.textContent = "Hide Hidden";
- }
- async function hideAllHiddenThreads() {
- showHiddenMode = false;
- await applyHiddenThreads();
- const btn = document.getElementById("ss-show-hidden-btn");
- if (btn) btn.textContent = "Show Hidden";
- }
- async function toggleShowHiddenThreads() {
- if (showHiddenMode) {
- await hideAllHiddenThreads();
- } else {
- await showAllHiddenThreads();
- }
- }
- function addShowHiddenButton() {
- if (document.getElementById("ss-show-hidden-btn")) return;
- const refreshBtn = document.querySelector("#catalogRefreshButton");
- if (!refreshBtn) return;
- const btn = document.createElement("button");
- btn.id = "ss-show-hidden-btn";
- btn.className = "catalogLabel";
- btn.type = "button";
- btn.textContent = "Show Hidden";
- btn.style.marginRight = "8px";
- btn.addEventListener("click", toggleShowHiddenThreads);
- refreshBtn.parentNode.insertBefore(btn, refreshBtn);
- }
- function hideThreadsOnRefresh() {
- if (!window.pageType?.isCatalog) return;
- onReady(addShowHiddenButton);
- onReady(applyHiddenThreads);
- const catalogContainer = document.querySelector(".catalogWrapper, .catalogDiv");
- if (catalogContainer) {
- catalogContainer.addEventListener("click", onCatalogCellClick, true);
- }
- }
- hideThreadsOnRefresh();
- const catalogDivObs = observeSelector('.catalogDiv', { childList: true, subtree: false });
- if (catalogDivObs) {
- const debouncedApply = debounce(applyHiddenThreads, 50);
- catalogDivObs.addHandler(function catalogHidingHandler() {
- debouncedApply();
- });
- }
- }
- (function noCaptchaHistory() {
- const captchaInput = document.getElementById("QRfieldCaptcha");
- if (captchaInput) {
- captchaInput.autocomplete = "off";
- }
- })();
- function preventFooterScrollIntoView() {
- const footer = document.getElementById('footer');
- if (footer && !footer._scrollBlocked) {
- footer._scrollBlocked = true;
- footer.scrollIntoView = function () {
- return;
- };
- }
- }
- (function moveFileUploadsBelowOp() {
- if (window.pageType?.isCatalog) {
- return;
- } else if (opHeadTitle && innerOP) {
- innerOP.insertBefore(opHeadTitle, innerOP.firstChild);
- }
- })();
- document.addEventListener('click', function (e) {
- const a = e.target.closest('.panelBacklinks > a');
- if (a) {
- setTimeout(() => {
- a.classList.toggle('reply-inlined');
- }, 0);
- return;
- }
- const b = e.target.closest('a.quoteLink');
- if (b) {
- setTimeout(() => {
- b.classList.toggle('quote-inlined');
- }, 0);
- }
- });
- async function updateNotif() {
- const VERSION_KEY = "8chanSS_version";
- let storedVersion = null;
- try {
- storedVersion = await GM.getValue(VERSION_KEY, null);
- } catch (err) {
- console.error("[8chanSS] Failed to get stored script version:", err);
- }
- if (storedVersion !== VERSION) {
- if (storedVersion !== null) {
- let tries = 0;
- while (typeof window.callPageToast !== "function" && tries < 20) {
- await new Promise(res => setTimeout(res, 100));
- tries++;
- }
- if (typeof window.callPageToast === "function") {
- window.callPageToast(
- `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>.`,
- "blue",
- 15000
- );
- }
- }
- try {
- await GM.setValue(VERSION_KEY, VERSION);
- } catch (err) {
- console.error("[8chanSS] Failed to store script version:", err);
- }
- }
- }
- });
Add Comment
Please, Sign In to add comment