// ==UserScript== // @name 8chanSS // @version 1.34.0 // @namespace 8chanSS // @description Userscript to style 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 // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/533268/8chanSS.user.js // @updateURL https://update.greasyfork.icu/scripts/533268/8chanSS.meta.js // ==/UserScript== (function () { const userTheme = localStorage.selectedTheme; if (!userTheme) return; const swapTheme = () => { const themeLink = Array.from( document.getElementsByTagName("link") ).find( (link) => link.rel === "stylesheet" && /\/\.static\/css\/themes\//.test(link.href) ); if (themeLink) { const themeBase = themeLink.href.replace(/\/[^\/]+\.css$/, "/"); themeLink.href = themeBase + userTheme + ".css"; } }; onReady(swapTheme); onReady(function () { const themeSelector = document.getElementById("themeSelector"); if (themeSelector) { for (let i = 0; i < themeSelector.options.length; i++) { if ( themeSelector.options[i].value === userTheme || themeSelector.options[i].text === userTheme ) { themeSelector.selectedIndex = i; break; } } } }); })(); (function () { function updateLocalStorage(removeKeys = [], setMap = {}) { for (const key of removeKeys) { localStorage.removeItem(key); } for (const [key, value] of Object.entries(setMap)) { localStorage.setItem(key, value); } } try { updateLocalStorage( ["hoveringImage"], {} ); } catch (e) { } })(); function onReady(fn) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", fn, { once: true }); } else { fn(); } } onReady(async function () { const scriptSettings = { site: { alwaysShowTW: { label: "Pin Thread Watcher", default: false }, enableHeaderCatalogLinks: { label: "Header Catalog Links", default: true, subOptions: { openInNewTab: { label: "Always open in new tab", default: false, } } }, enableBottomHeader: { label: "Bottom Header", default: false }, enableScrollSave: { label: "Save Scroll Position", default: true, subOptions: { showUnreadLine: { label: "Show Unread Line", default: true, } } }, enableScrollArrows: { label: "Show Up/Down Arrows", default: false }, hoverVideoVolume: { label: "Hover Media Volume (0-100%)", default: 50, type: "number", min: 0, max: 100 } }, threads: { enableThreadImageHover: { label: "Thread Image Hover", default: true }, enableNestedReplies: { label: "Enabled Nested Replies", default: false }, enableStickyQR: { label: "Enable Sticky Quick Reply", default: false }, fadeQuickReply: { label: "Fade Quick Reply", default: false }, watchThreadOnReply: { label: "Watch Thread on Reply", default: true }, scrollToBottom: { label: "Don't Scroll to Bottom on Reply", default: true }, beepOnYou: { label: "Beep on (You)", default: false }, notifyOnYou: { label: "Notify when (You) (!)", default: true, subOptions: { customMessage: { label: "Custom Notification", default: "", type: "text", maxLength: 8 } } }, blurSpoilers: { label: "Blur Spoilers", default: false, subOptions: { removeSpoilers: { label: "Remove Spoilers", default: false } } }, deleteSavedName: { label: "Delete Name Checkbox", default: true } }, catalog: { enableCatalogImageHover: { label: "Catalog Image Hover", default: true }, enableThreadHiding: { label: "Enable Thread Hiding", default: false } }, styling: { _siteTitle: { 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 }, _threadTitle: { type: "title", label: ":: Thread Styling" }, _stylingSection2: { type: "separator" }, highlightOnYou: { label: "Highlight (You) posts", default: true }, 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 }, hideHiddenPostStub: { label: "Hide Stubs of Hidden Posts", default: false, } }, }; const flatSettings = {}; function flattenSettings() { Object.keys(scriptSettings).forEach((category) => { Object.keys(scriptSettings[category]).forEach((key) => { flatSettings[key] = scriptSettings[category][key]; if (!scriptSettings[category][key].subOptions) return; Object.keys(scriptSettings[category][key].subOptions).forEach( (subKey) => { const fullKey = `${key}_${subKey}`; flatSettings[fullKey] = scriptSettings[category][key].subOptions[subKey]; } ); }); }); } flattenSettings(); async function getSetting(key) { if (!flatSettings[key]) { console.warn(`Setting key not found: ${key}`); return false; } let val = await GM.getValue("8chanSS_" + key, null); if (val === null) return flatSettings[key].default; if (flatSettings[key].type === "number") return Number(val); if (flatSettings[key].type === "text") return String(val).replace(/[<>"']/g, "").slice(0, flatSettings[key].maxLength || 32); return val === "true"; } async function setSetting(key, value) { await GM.setValue("8chanSS_" + key, String(value)); } 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", hideAnnouncement: "hide-announcement", hidePanelMessage: "hide-panelmessage", highlightOnYou: "highlight-you", threadHideCloseBtn: "hide-close-btn" }; if (enableSidebar && !enableSidebar_leftSidebar) { document.documentElement.classList.add("ss-sidebar"); } else { document.documentElement.classList.remove("ss-sidebar"); } for (const [settingKey, className] of Object.entries(classToggles)) { if (await getSetting(settingKey)) { document.documentElement.classList.add(className); } else { document.documentElement.classList.remove(className); } } const urlClassMap = [ { pattern: /\/catalog\.html$/i, className: "is-catalog" }, { pattern: /\/res\/[^/]+\.html$/i, className: "is-thread" }, { pattern: /\/[^/]+\/(#)?$/i, className: "is-index" }, ]; const currentPath = window.location.pathname.toLowerCase() + window.location.hash; urlClassMap.forEach(({ pattern, className }) => { if (pattern.test(currentPath)) { document.documentElement.classList.add(className); } else { document.documentElement.classList.remove(className); } }); } featureCssClassToggles(); 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"; } } onReady(featureSidebar); 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: "#333", 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: ["Ctrl", "Q"], action: "Toggle Quick Reply" }, { keys: ["Ctrl", "Enter"], action: "Submit post" }, { keys: ["Escape"], action: "Clear textarea and hide Quick Reply" }, { keys: ["ALT", "W"], action: "Watch Thread" }, { 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: "Doom 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; } const currentPath = window.location.pathname.toLowerCase(); const currentHost = window.location.hostname.toLowerCase(); let css = ""; if (/^8chan\.(se|moe)$/.test(currentHost)) { css += ":not(.is-catalog) body{margin:0}#sideCatalogDiv{z-index:200;background:var(--background-gradient)}#navFadeEnd,#navFadeMid,:root.hide-announcement #dynamicAnnouncement,:root.hide-close-btn .inlineQuote>.innerPost>.postInfo.title>a:first-child,:root.hide-panelmessage #panelMessage,:root.hide-posting-form #postingForm{display:none}:root.hide-defaultBL #navTopBoardsSpan{display:none!important}:root.is-catalog.show-catalog-form #postingForm{display:block!important}footer{visibility:hidden;height:0}nav.navHeader{z-index:300}: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.fit-replies :not(.hidden).innerPost{margin-left:10px;display:flow-root}:root.fit-replies :not(.hidden,.inlineQuote).innerPost{margin-left:0}:root.fit-replies .quoteTooltip{display:table!important}#watchedMenu .floatingContainer{overflow-x:hidden;overflow-wrap:break-word}.watchedCellLabel a::before{content:attr(data-board);color:#aaa;margin-right:4px;font-weight:700}.watchButton.watched-active::before{color:#dd003e!important}#watchedMenu{font-size:smaller;padding:5px!important;box-shadow:-3px 3px 2px 0 rgba(0,0,0,.19)}#watchedMenu,#watchedMenu .floatingContainer{min-width:200px}.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}.innerUtility.top{margin-top:2em;background-color:transparent!important;color:var(--link-color)!important}.innerUtility.top a{color:var(--link-color)!important}.bumpLockIndicator::after{padding-right:3px}.floatingMenu.focused{z-index:305!important}.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}"; } if (/\/res\/[^/]+\.html$/.test(currentPath)) { 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}.floatingMenu{padding:0!important}#qrFilesBody{max-width:300px}#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 0 auto;width:60%}:root.disable-banner #bannerImage{display:none}: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;box-shadow:-3px 3px 2px 0 rgba(0,0,0,.19)}.reply-inlined{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)}:root.highlight-you .innerPost:has(> .postInfo.title > .youName){border-left:dashed #68b723 3px}:root.highlight-you .innerPost:not(:has(> .postInfo.title > .youName)):has(.divMessage > .quoteLink.you){border-left:solid #dd003e 3px}.originalNameLink{display:inline;overflow-wrap:anywhere;white-space:normal}.multipleUploads .uploadCell:not(.expandedCell){max-width:215px}.imgExpanded,video{max-height:90vh!important;object-fit:contain;width:auto!important}.postCell::before{display:inline!important;height:auto!important}"; } if (/\/catalog\.html$/.test(currentPath)) { css += "#dynamicAnnouncement{display:none}#postingForm{margin:2em auto}"; } if (!document.getElementById('8chSS')) { const style = document.createElement('style'); style.id = '8chSS'; style.textContent = css; document.head.appendChild(style); } if (await getSetting("enableScrollSave")) { featureSaveScroll(); } if (await getSetting("watchThreadOnReply")) { featureWatchThreadOnReply(); } if (await getSetting("blurSpoilers")) { featureBlurSpoilers(); } if (await getSetting("enableHeaderCatalogLinks")) { featureHeaderCatalogLinks(); } if (await getSetting("deleteSavedName")) { featureDeleteNameCheckbox(); } if (await getSetting("enableScrollArrows")) { featureScrollArrows(); } if (await getSetting("alwaysShowTW")) { featureAlwaysShowTW(); } if (await getSetting("scrollToBottom")) { preventFooterScrollIntoView(); } if (await getSetting("enableThreadHiding")) { featureCatalogThreadHideShortcut(); } if (await getSetting("enableNestedReplies")) { localStorage.setItem("inlineReplies", "true"); featureNestedReplies(); } async function initImageHover() { const isCatalogPage = /\/catalog\.html$/.test(window.location.pathname.toLowerCase()); let enabled = false; if (isCatalogPage) { enabled = await getSetting("enableCatalogImageHover"); } else { enabled = await getSetting("enableThreadImageHover"); } if (enabled) { featureImageHover(); } } initImageHover(); 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 = "4rem"; menu.style.left = "20rem"; menu.style.zIndex = "99999"; menu.style.background = "#222"; menu.style.color = "#fff"; menu.style.padding = "0"; menu.style.borderRadius = "8px"; 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 = "450px"; 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.background = "#333"; header.style.padding = "5px 18px 5px"; header.style.borderTopLeftRadius = "8px"; header.style.borderTopRightRadius = "8px"; 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.border = "none"; closeBtn.style.color = "#fff"; 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 tabNav = document.createElement("div"); tabNav.style.display = "flex"; tabNav.style.borderBottom = "1px solid #444"; tabNav.style.background = "#2a2a2a"; const tabContent = document.createElement("div"); tabContent.style.padding = "15px 16px"; tabContent.style.maxHeight = "60vh"; tabContent.style.overflowY = "auto"; 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), }, 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 ? "#333" : "transparent"; tabButton.style.border = "none"; tabButton.style.borderRight = "1px solid #444"; tabButton.style.color = "#fff"; tabButton.style.padding = "8px 15px"; tabButton.style.margin = "5px 0 0 0"; tabButton.style.cursor = "pointer"; tabButton.style.flex = "1"; tabButton.style.fontSize = "14px"; tabButton.style.transition = "background 0.2s"; if (index === 0) { tabButton.style.borderTopLeftRadius = "8px"; tabButton.style.margin = "5px 0 0 5px"; } if (index === arr.length - 1) { tabButton.style.borderTopRightRadius = "8px"; 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 = "#333"; }); 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.background = "#4caf50"; saveBtn.style.color = "#fff"; 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.background = "#dd3333"; resetBtn.style.color = "#fff"; 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. - Ver. 1.34.0'; 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.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 { 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; } 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 featureSaveScroll() { const MAX_PAGES = 50; const currentPage = window.location.origin + window.location.pathname + window.location.search; const hasAnchor = !!window.location.hash; const threadPagePattern = /^\/[^/]+\/res\/[^/]+\.html$/i; function isThreadPage(urlPath) { return threadPagePattern.test(urlPath); } async function getSavedScrollData() { const savedData = await GM.getValue( `8chanSS_scrollPosition_${currentPage}`, null ); if (!savedData) return null; try { return JSON.parse(savedData); } catch (e) { return null; } } async function saveScrollPosition() { if (!isThreadPage(window.location.pathname)) return; if (!(await getSetting("enableScrollSave"))) return; const scrollPosition = window.scrollY; const timestamp = Date.now(); const savedData = await getSavedScrollData(); if (savedData && typeof savedData.position === "number") { if (scrollPosition <= savedData.position) { return; } } await GM.setValue( `8chanSS_scrollPosition_${currentPage}`, JSON.stringify({ position: scrollPosition, timestamp: timestamp, }) ); await manageScrollStorage(); } async function manageScrollStorage() { const allKeys = await GM.listValues(); const scrollKeys = allKeys.filter((key) => key.startsWith("8chanSS_scrollPosition_") ); if (scrollKeys.length > MAX_PAGES) { const keyData = await Promise.all( scrollKeys.map(async (key) => { let data; try { const savedValue = await GM.getValue(key, null); data = savedValue ? JSON.parse(savedValue) : { position: 0, timestamp: 0 }; } catch (e) { data = { position: 0, timestamp: 0 }; } return { key: key, timestamp: data.timestamp || 0, }; }) ); keyData.sort((a, b) => a.timestamp - b.timestamp); const keysToRemove = keyData.slice(0, keyData.length - MAX_PAGES); for (const item of keysToRemove) { await GM.deleteValue(item.key); } } } async function restoreScrollPosition() { if (!isThreadPage(window.location.pathname)) return; if (!(await getSetting("enableScrollSave"))) return; const savedData = await getSavedScrollData(); if (!savedData || typeof savedData.position !== "number") return; const position = savedData.position; await GM.setValue( `8chanSS_scrollPosition_${currentPage}`, JSON.stringify({ position: position, timestamp: Date.now(), }) ); if (hasAnchor) { setTimeout(() => addUnreadLineAtViewportCenter(position), 100); return; } if (!isNaN(position)) { window.scrollTo(0, position); setTimeout(() => addUnreadLineAtViewportCenter(position), 100); } } async function addUnreadLineAtViewportCenter(scrollPosition) { if (!(await getSetting("enableScrollSave_showUnreadLine"))) { return; } const divPosts = document.querySelector(".divPosts"); if (!divPosts) return; const centerX = window.innerWidth / 2; const centerY = (typeof scrollPosition === "number") ? (window.innerHeight / 2) + (scrollPosition - window.scrollY) : window.innerHeight / 2; let el = document.elementFromPoint(centerX, centerY); while (el && el !== divPosts && (!el.classList || !el.classList.contains("postCell"))) { el = el.parentElement; } if (!el || el === divPosts || !el.id) return; if (el.parentElement !== divPosts) return; const oldMarker = document.getElementById("unread-line"); if (oldMarker && oldMarker.parentNode) { oldMarker.parentNode.removeChild(oldMarker); } const marker = document.createElement("hr"); marker.id = "unread-line"; if (el.nextSibling) { divPosts.insertBefore(marker, el.nextSibling); } else { divPosts.appendChild(marker); } } window.addEventListener("beforeunload", () => { saveScrollPosition(); }); window.addEventListener("load", async () => { await restoreScrollPosition(); }); await restoreScrollPosition(); } async function removeUnreadLineIfAtBottom() { if (!(await getSetting("enableScrollSave_showUnreadLine"))) { return; } const margin = 20; if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - margin)) { const oldMarker = document.getElementById("unread-line"); if (oldMarker && oldMarker.parentNode) { oldMarker.parentNode.removeChild(oldMarker); } } } window.addEventListener("scroll", removeUnreadLineIfAtBottom); 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.href += "/catalog.html"; if (openInNewTab) { link.target = "_blank"; link.rel = "noopener noreferrer"; } else { link.target = ""; link.rel = ""; } } } } } appendCatalogToLinks(); const observer = new MutationObserver(appendCatalogToLinks); const config = { childList: true, subtree: true }; const navboardsSpan = document.getElementById("navBoardsSpan"); if (navboardsSpan) { observer.observe(navboardsSpan, config); } } function featureImageHover() { const MEDIA_MAX_WIDTH = "90vw"; const MEDIA_OPACITY_LOADING = "0.75"; const MEDIA_OPACITY_LOADED = "1"; const MEDIA_OFFSET = 2; const MEDIA_BOTTOM_MARGIN = 3; const AUDIO_INDICATOR_TEXT = "▶ Playing audio..."; function getMediaOffset() { return window.innerWidth * (MEDIA_OFFSET / 100); } function getMediaBottomMargin() { return window.innerHeight * (MEDIA_BOTTOM_MARGIN / 100); } 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 MEDIA_OFFSET_PX = getMediaOffset(); const MEDIA_BOTTOM_MARGIN_PX = getMediaBottomMargin(); const SCROLLBAR_WIDTH = window.innerWidth - document.documentElement.clientWidth; let x = event.clientX + MEDIA_OFFSET_PX; x = clamp(x, 0, vw - mw - SCROLLBAR_WIDTH); let 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 positionFloatingMediaInitial(event) { if (!floatingMedia) return; const vw = window.innerWidth; const vh = window.innerHeight; const mw = floatingMedia.offsetWidth || 320; const mh = floatingMedia.offsetHeight || 240; const MEDIA_OFFSET_PX = getMediaOffset(); const MEDIA_BOTTOM_MARGIN_PX = getMediaBottomMargin(); const SCROLLBAR_WIDTH = window.innerWidth - document.documentElement.clientWidth; let x = vw / 2, y = vh / 2; if (event && typeof event.clientX === "number" && typeof event.clientY === "number") { x = event.clientX + MEDIA_OFFSET_PX; x = clamp(x, 0, vw - mw - SCROLLBAR_WIDTH); y = event.clientY; const maxY = vh - mh - MEDIA_BOTTOM_MARGIN_PX; y = Math.max(0, Math.min(y, maxY)); } else { x = clamp((vw - mw) / 2, 0, vw - mw - SCROLLBAR_WIDTH); const maxY = vh - mh - MEDIA_BOTTOM_MARGIN_PX; y = clamp((vh - mh - MEDIA_BOTTOM_MARGIN_PX) / 2, 0, 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.removeAttribute("src"); floatingMedia.load(); } catch { } } floatingMedia.remove(); floatingMedia = null; } if (currentAudioIndicator && currentAudioIndicator.parentNode) { currentAudioIndicator.parentNode.removeChild(currentAudioIndicator); currentAudioIndicator = null; } } function getFullMediaSrc(thumbNode, filemime) { if (!thumbNode || !filemime) return null; const thumbnailSrc = thumbNode.getAttribute("src"); if (/\/t_/.test(thumbnailSrc)) { let base = thumbnailSrc.replace(/\/t_/, "/"); base = base.replace(/\.(jpe?g|png|gif|webp|webm|mp4|ogg|mp3|m4a|wav)$/i, ""); const mimeToExt = { "image/jpeg": ".jpg", "image/jpg": ".jpg", "image/png": ".png", "image/gif": ".gif", "image/webp": ".webp", "image/bmp": ".bmp", "video/mp4": ".mp4", "video/webm": ".webm", "audio/ogg": ".ogg", "audio/mpeg": ".mp3", "audio/x-m4a": ".m4a", "audio/x-wav": ".wav", }; const ext = mimeToExt[filemime.toLowerCase()]; if (!ext) return null; return base + ext; } if ( /\/spoiler\.png$/i.test(thumbnailSrc) || /\/custom\.spoiler$/i.test(thumbnailSrc) || /\/audioGenericThumb\.png$/i.test(thumbnailSrc) ) { const parentA = thumbNode.closest("a.linkThumb, a.imgLink"); if (parentA && parentA.getAttribute("href")) { return parentA.getAttribute("href"); } return null; } return null; } 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", png: "image/png", gif: "image/gif", webp: "image/webp", bmp: "image/bmp", mp4: "video/mp4", webm: "video/webm", 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/"); } else if (thumb.classList.contains("originalNameLink")) { const href = thumb.getAttribute("href"); if (!href) return; const ext = href.split(".").pop().toLowerCase(); if (["mp3", "ogg", "m4a", "wav"].includes(ext)) { filemime = { ogg: "audio/ogg", mp3: "audio/mpeg", m4a: "audio/x-m4a", wav: "audio/wav", }[ext]; fullSrc = href; isAudio = true; } } if (!fullSrc || !filemime) return; if (isAudio) { const container = thumb.tagName === "IMG" ? thumb.closest("a.linkThumb, a.imgLink") : thumb; if (container && !container.style.position) { container.style.position = "relative"; } floatingMedia = document.createElement("audio"); floatingMedia.src = fullSrc; floatingMedia.controls = false; floatingMedia.style.display = "none"; let volume = 0.5; try { if (typeof getSetting === "function") { const v = await getSetting("hoverVideoVolume"); if (typeof v === "number" && !isNaN(v)) { volume = v / 100; } } } catch { } floatingMedia.volume = clamp(volume, 0, 1); document.body.appendChild(floatingMedia); floatingMedia.play().catch(() => { }); const indicator = document.createElement("div"); indicator.classList.add("audio-preview-indicator"); indicator.textContent = AUDIO_INDICATOR_TEXT; container.appendChild(indicator); currentAudioIndicator = indicator; const cleanup = () => cleanupFloatingMedia(); thumb.addEventListener("mouseleave", cleanup, { once: true }); container.addEventListener("click", cleanup, { once: true }); window.addEventListener("scroll", cleanup, { once: true }); cleanupFns.push(() => thumb.removeEventListener("mouseleave", cleanup)); cleanupFns.push(() => container.removeEventListener("click", cleanup)); cleanupFns.push(() => window.removeEventListener("scroll", cleanup)); return; } floatingMedia = isVideo ? document.createElement("video") : document.createElement("img"); floatingMedia.src = fullSrc; 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 - getMediaBottomMargin(); floatingMedia.style.maxHeight = `${availableHeight}px`; if (isVideo) { floatingMedia.autoplay = true; floatingMedia.loop = true; floatingMedia.muted = false; floatingMedia.playsInline = true; } document.body.appendChild(floatingMedia); function initialPlacement() { if (lastMouseEvent) { positionFloatingMedia(lastMouseEvent); } } function enableMouseMove() { document.addEventListener("mousemove", mouseMoveHandler); cleanupFns.push(() => document.removeEventListener("mousemove", mouseMoveHandler)); } function mouseMoveHandler(ev) { positionFloatingMedia(ev); } if (isVideo) { floatingMedia.onloadeddata = function () { initialPlacement(); enableMouseMove(); if (floatingMedia) floatingMedia.style.opacity = MEDIA_OPACITY_LOADED; }; } else { floatingMedia.onload = function () { initialPlacement(); enableMouseMove(); if (floatingMedia) floatingMedia.style.opacity = MEDIA_OPACITY_LOADED; }; } floatingMedia.onerror = cleanupFloatingMedia; function leaveHandler() { cleanupFloatingMedia(); } thumb.addEventListener("mouseleave", leaveHandler, { once: true }); window.addEventListener("scroll", leaveHandler, { 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; } }); root.querySelectorAll("a.originalNameLink").forEach(link => { const href = link.getAttribute("href") || ""; const ext = href.split(".").pop().toLowerCase(); if ( ["mp3", "wav", "ogg", "m4a"].includes(ext) && !link._audioHoverBound ) { link.addEventListener("mouseenter", onThumbEnter); link._audioHoverBound = true; } }); } attachThumbListeners(); new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { attachThumbListeners(node); } } } }).observe(document.body, { childList: true, subtree: true }); } function featureNestedReplies() { function ensureReplyPreviewPlacement(root = document) { root.querySelectorAll('.innerPost').forEach(innerPost => { const divMessage = innerPost.querySelector('.divMessage'); if (!divMessage) return; const replyPreview = innerPost.querySelector('.replyPreview'); if (replyPreview && replyPreview.nextSibling !== divMessage) { innerPost.insertBefore(replyPreview, divMessage); } innerPost.querySelectorAll('.inlineQuote').forEach(inlineQuote => { if (inlineQuote.nextSibling !== divMessage) { innerPost.insertBefore(inlineQuote, divMessage); } }); }); } ensureReplyPreviewPlacement(); const observer = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; if (node.matches && node.matches('.innerPost')) { ensureReplyPreviewPlacement(node); } else if (node.querySelectorAll) { node.querySelectorAll('.innerPost').forEach(innerPost => { ensureReplyPreviewPlacement(innerPost); }); } } } }); const postsContainer = document.querySelector('.divPosts'); if (postsContainer) { observer.observe(postsContainer, { childList: true, subtree: true }); } document.addEventListener('click', function (e) { const a = e.target.closest('.panelBacklinks > a'); if (!a) return; setTimeout(() => { a.classList.toggle('reply-inlined'); }, 0); }); } function featureBlurSpoilers() { function revealSpoilers() { const spoilerLinks = document.querySelectorAll("a.imgLink"); spoilerLinks.forEach(async (link) => { const img = link.querySelector("img"); if (!img) return; const isCustomSpoiler = img.src.includes("/custom.spoiler"); const isNotThumbnail = !img.src.includes("/.media/t_"); if (isNotThumbnail || isCustomSpoiler) { let href = link.getAttribute("href"); if (!href) return; const match = href.match(/\/\.media\/([^\/]+)\.[a-zA-Z0-9]+$/); if (!match) return; const transformedSrc = `/.media/t_${match[1]}`; img.src = transformedSrc; if (await getSetting("blurSpoilers_removeSpoilers")) { img.style.filter = ""; img.style.transition = ""; img.onmouseover = null; img.onmouseout = null; return; } else { img.style.filter = "blur(5px)"; img.style.transition = "filter 0.3s ease"; img.addEventListener("mouseover", () => { img.style.filter = "none"; }); img.addEventListener("mouseout", () => { img.style.filter = "blur(5px)"; }); } } }); } revealSpoilers(); const observer = new MutationObserver(revealSpoilers); observer.observe(document.body, { childList: true, subtree: true }); } function decodeHtmlEntitiesTwice(html) { const txt = document.createElement('textarea'); txt.innerHTML = html; const once = txt.value; txt.innerHTML = once; return txt.value; } function highlightMentions() { document.querySelectorAll("#watchedMenu .watchedCell").forEach((cell) => { const notification = cell.querySelector(".watchedCellLabel span.watchedNotification"); const labelLink = cell.querySelector(".watchedCellLabel a"); const watchedCellLabel = cell.querySelector(".watchedCellLabel"); if (labelLink) { const originalHtml = labelLink.innerHTML; const decodedText = decodeHtmlEntitiesTwice(originalHtml); if (labelLink.textContent !== decodedText) { labelLink.textContent = decodedText; } } if (labelLink) { 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)) { const watchButton = document.querySelector(".opHead .watchButton"); if (watchButton) { watchButton.style.color = "var(--board-title-color)"; watchButton.title = "Watched"; } } } if (notification && notification.textContent.includes("(you)")) { labelLink.style.color = "var(--board-title-color)"; if (watchedCellLabel && !watchedCellLabel.querySelector(".you-mention-label")) { const youLabel = document.createElement("span"); youLabel.className = "you-mention-label"; youLabel.textContent = " - (You)"; youLabel.style.color = "var(--board-title-color)"; watchedCellLabel.appendChild(youLabel); } } else { labelLink.style.color = ""; const youLabel = watchedCellLabel?.querySelector(".you-mention-label"); if (youLabel) { youLabel.remove(); } } } }); } highlightMentions(); const watchedMenu = document.getElementById("watchedMenu"); if (watchedMenu) { const observer = new MutationObserver(() => { highlightMentions(); }); observer.observe(watchedMenu, { childList: true, subtree: true }); } async function featureWatchThreadOnReply() { 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); } } document.addEventListener("keydown", async function (event) { if ( event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey && (event.key === "w" || event.key === "W") ) { event.preventDefault(); if ( typeof getSetting === "function" && (await getSetting("watchThreadOnReply")) ) { const btn = document.querySelector(".watchButton"); if (btn && !btn.classList.contains("watched-active")) { btn.click(); setTimeout(() => { btn.classList.add("watched-active"); }, 100); } } } }); async function featureAlwaysShowTW() { if (!(await getSetting("alwaysShowTW"))) return; function showThreadWatcher() { const watchedMenu = document.getElementById("watchedMenu"); if (watchedMenu) { watchedMenu.style.display = "flex"; } } function addCloseListener() { const watchedMenu = document.getElementById("watchedMenu"); if (!watchedMenu) return; const closeBtn = watchedMenu.querySelector(".close-btn"); if (closeBtn) { closeBtn.addEventListener("click", () => { watchedMenu.style.display = "none"; }); } } onReady(() => { showThreadWatcher(); addCloseListener(); }); } function featureMarkYourPost() { 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 = "markYourPostMenuEntry"; const MENU_SELECTOR = ".floatingList.extraMenu"; function getTYous() { try { const val = localStorage.getItem(T_YOUS_KEY); if (!val) return []; return JSON.parse(val); } catch { return []; } } function setTYous(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'); setTimeout(() => { const menu = document.querySelector(MENU_SELECTOR); if (menu && postCell) { menu.setAttribute('data-post-id', postCell.id); } }, 0); } }); function getPostIdFromMenu(menu) { return menu.getAttribute('data-post-id') || null; } function toggleYouNameClass(postId, add) { const postCell = document.getElementById(postId); if (!postCell) return; const nameLink = postCell.querySelector(".linkName.noEmailName"); if (nameLink) { nameLink.classList.toggle("youName", add); } } function addMenuEntries(root = document) { root.querySelectorAll(MENU_SELECTOR).forEach(menu => { const ul = menu.querySelector("ul"); if (!ul || ul.querySelector("." + MENU_ENTRY_CLASS)) return; const reportLi = Array.from(ul.children).find( li => li.textContent.trim().toLowerCase() === "report" ); const li = document.createElement("li"); li.className = MENU_ENTRY_CLASS; li.style.cursor = "pointer"; const postId = getPostIdFromMenu(menu); const tYous = getTYous(); const isMarked = postId && tYous.includes(Number(postId)); li.textContent = isMarked ? "Unmark as Your Post" : "Mark as Your Post"; if (reportLi) { ul.insertBefore(li, reportLi); } else { ul.insertBefore(li, ul.firstChild); } li.addEventListener("click", function (e) { e.stopPropagation(); const postId = getPostIdFromMenu(menu); if (!postId) return; let tYous = getTYous(); const numericPostId = Number(postId); const idx = tYous.indexOf(numericPostId); if (idx === -1) { tYous.push(numericPostId); setTYous(tYous); toggleYouNameClass(postId, true); li.textContent = "Unmark as Your Post"; } else { tYous.splice(idx, 1); setTYous(tYous); toggleYouNameClass(postId, false); li.textContent = "Mark as Your Post"; } }); window.addEventListener("storage", function (event) { if (event.key === T_YOUS_KEY) { const tYous = getTYous(); const isMarked = postId && tYous.includes(Number(postId)); li.textContent = isMarked ? "Unmark as Your Post" : "Mark as Your Post"; } }); }); } const observer = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; if (node.matches && node.matches(MENU_SELECTOR)) { addMenuEntries(node.parentNode || node); } else if (node.querySelectorAll) { node.querySelectorAll(MENU_SELECTOR).forEach(menu => { addMenuEntries(menu.parentNode || menu); }); } } } }); observer.observe(document.body, { childList: true, subtree: true }); } onReady(featureMarkYourPost); 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", () => { const footer = document.getElementById("footer"); if (footer) { footer.scrollIntoView({ behavior: "smooth", block: "end" }); } else { window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth", }); } }); document.body.appendChild(upBtn); document.body.appendChild(downBtn); } function featureDeleteNameCheckbox() { const nameExists = document.getElementById("qr-name-row"); if (nameExists && nameExists.classList.contains("hidden")) { return; } const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.id = "saveNameCheckbox"; checkbox.classList.add("postingCheckbox"); const label = document.createElement("label"); label.htmlFor = "saveNameCheckbox"; label.textContent = "Delete Name"; label.title = "Delete Name on refresh"; const alwaysUseBypassCheckbox = document.getElementById("qralwaysUseBypassCheckBox"); if (!alwaysUseBypassCheckbox) { return; } alwaysUseBypassCheckbox.parentNode.insertBefore(checkbox, alwaysUseBypassCheckbox); alwaysUseBypassCheckbox.parentNode.insertBefore(label, checkbox.nextSibling); const savedCheckboxState = localStorage.getItem("8chanSS_deleteNameCheckbox") === "true"; checkbox.checked = savedCheckboxState; const nameInput = document.getElementById("qrname"); if (nameInput) { if (checkbox.checked) { nameInput.value = ""; localStorage.removeItem("name"); } checkbox.addEventListener("change", function () { localStorage.setItem("8chanSS_deleteNameCheckbox", checkbox.checked); }); } } function featureBeepOnYou() { const beep = new Audio( "data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA" ); window.originalTitle = document.title; let isNotifying = false; function playBeep() { if (beep.paused) { beep.play().catch((e) => console.warn("Beep failed:", e)); } else { beep.addEventListener("ended", () => beep.play(), { once: true }); } } const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach(async (node) => { if ( node.nodeType === 1 && node.querySelector && node.querySelector("a.quoteLink.you") ) { if (node.closest('.innerPost')) { return; } if (await getSetting("beepOnYou")) { playBeep(); } if (await getSetting("notifyOnYou")) { featureNotifyOnYou(); } } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); async function featureNotifyOnYou() { if (!window.isNotifying && !document.hasFocus()) { window.isNotifying = true; let customMsg = await getSetting("notifyOnYou_customMessage"); if (!customMsg) customMsg = "(!) "; document.title = customMsg + " " + window.originalTitle; if (!window.notifyFocusListenerAdded) { window.addEventListener("focus", () => { if (window.isNotifying) { document.title = window.originalTitle; window.isNotifying = false; } }); window.notifyFocusListenerAdded = true; } } } window.addEventListener("focus", () => { if (isNotifying) { document.title = window.originalTitle; isNotifying = false; } }); } featureBeepOnYou(); document.addEventListener("keydown", async function (event) { 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"; } }); async function submitWithCtrlEnter(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); } } } } const replyTextarea = document.getElementById("qrbody"); if (replyTextarea) { replyTextarea.addEventListener("keydown", submitWithCtrlEnter); } function toggleQR(event) { if (event.ctrlKey && (event.key === "q" || event.key === "Q")) { const hiddenDiv = document.getElementById("quick-reply"); if ( hiddenDiv.style.display === "none" || hiddenDiv.style.display === "" ) { hiddenDiv.style.display = "block"; setTimeout(() => { const textarea = document.getElementById("qrbody"); if (textarea) { textarea.focus(); } }, 50); } else { hiddenDiv.style.display = "none"; } } } document.addEventListener("keydown", toggleQR); function clearTextarea(event) { 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"; } } } document.addEventListener("keydown", clearTextarea); function featureScrollBetweenPosts() { let lastHighlighted = null; let lastType = null; let lastIndex = -1; 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; if ( lastType === (isOwnReply ? "own" : "reply") && lastHighlighted && (currentIndex = postCells.indexOf(lastHighlighted.closest('.postCell, .opCell'))) !== -1 ) { } else { 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) { if (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"; lastIndex = targetIndex; } } function onKeyDown(event) { if ( event.target && ( /^(input|textarea)$/i.test(event.target.tagName) || event.target.isContentEditable ) ) return; if (event.ctrlKey && event.shiftKey) { if (event.key === 'ArrowDown') { event.preventDefault(); scrollToReply(false, true); } else if (event.key === 'ArrowUp') { event.preventDefault(); scrollToReply(false, false); } } else if (event.ctrlKey) { if (event.key === 'ArrowDown') { event.preventDefault(); scrollToReply(true, true); } else if (event.key === 'ArrowUp') { event.preventDefault(); scrollToReply(true, false); } } } 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', onKeyDown); } featureScrollBetweenPosts(); const bbCodeCombinations = new Map([ ["s", ["[spoiler]", "[/spoiler]"]], ["b", ["'''", "'''"]], ["u", ["__", "__"]], ["i", ["''", "''"]], ["d", ["[doom]", "[/doom]"]], ["m", ["[moe]", "[/moe]"]], ["c", ["[code]", "[/code]"]], ]); function replyKeyboardShortcuts(ev) { const key = ev.key.toLowerCase(); if ( key === "c" && ev.altKey && !ev.ctrlKey && bbCodeCombinations.has(key) ) { ev.preventDefault(); const textBox = ev.target; 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; } return; } if ( ev.ctrlKey && !ev.altKey && bbCodeCombinations.has(key) && key !== "c" ) { ev.preventDefault(); const textBox = ev.target; 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; } return; } } document .getElementById("qrbody") ?.addEventListener("keydown", replyKeyboardShortcuts); function featureCatalogThreadHideShortcut() { 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 STORAGE_KEY = "8chanSS_hiddenCatalogThreads"; 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 (!/\/catalog\.html$/.test(window.location.pathname)) return; onReady(addShowHiddenButton); onReady(applyHiddenThreads); const catalogContainer = document.querySelector(".catalogWrapper, .catalogDiv"); if (catalogContainer) { catalogContainer.addEventListener("click", onCatalogCellClick, true); const observer = new MutationObserver(applyHiddenThreads); observer.observe(catalogContainer, { childList: true, subtree: true }); } } hideThreadsOnRefresh(); } 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 () { }; } } function moveUploadsBelowOP() { const panelUploads = document.querySelector('.panelUploads'); const opHeadTitle = document.querySelector('.opHead.title'); if (panelUploads && opHeadTitle) { opHeadTitle.appendChild(panelUploads); } } moveUploadsBelowOP(); });