// ==UserScript== // @name bagscript // @description bag script with anti bot features + more // @version 0.8.1.1 // @license MIT // @namespace 9e7f6239-592e-409b-913f-06e11cc5e545 // @include https://8chan.moe/v/res/* // @include https://8chan.se/v/res/* // @include https://8chan.moe/barchive/res/* // @include https://8chan.se/barchive/res/* // @include https://8chan.moe/test/res/* // @include https://8chan.se/test/res/* // @grant unsafeWindow // @run-at document-idle // @downloadURL none // ==/UserScript== // Script settings const ACTIVE_COLOR = "rgb(255, 211, 136)"; const BAD_COLOR = "#FAA"; const RUDE_FORMATS = ["JPEG", "JPG", "PNG"]; const SPOILER_BORDER = "3px solid red"; const THREAD_LOCKED_AT = 1500; const URL_PREFIX = window.location.href.split("/").slice(0, 4).join("/"); // Debug settings const DEBUG_TOOLS_VISIBLE = false; const DISABLE_YOU_BYPASS = true; const FORCE_NEXT_THREAD_FAIL = false; // Janny tool settings const BOT_BAN_REASON = "bot*"; // State let manualBypass; let defaultSpoilerSrc; const settings = {}; let threadsClosed = false; let menuVisible = false; // Tooltips / Info / Etc const NOT_A_JANNY = "You aren't a janny dumbass." const BOT_BAN_BUTTON_WARNING = "WARNING: The Bot Ban button will immediately issue a ban + delete by IP for the poster WITH NO CONFIRMATION. Are you sure you want to turn this on?"; // Loader (new MutationObserver((_, observer) => { const threadTitle = document.querySelector("div.opHead > span.labelSubject"); if (threadTitle) { observer.disconnect(); loadSettings(); loadMenu(); const subjectInput = document.querySelector(".subjectInput"); const threadTitle = document.querySelector("div.opHead > span.labelSubject").innerText.toUpperCase(); const titleSetting = settings?.threadSubject?.toUpperCase() ?? ""; if (threadTitle.includes(titleSetting)) { subjectMatched(); subjectInput.style.backgroundColor = "white"; } else { subjectInput.style.backgroundColor = BAD_COLOR; } } })).observe(document, {childList: true, subtree: true}); const subjectMatched = function() { const initialPosts = document.querySelectorAll(".postCell"); if (initialPosts.length >= THREAD_LOCKED_AT) { addNextThreadFakePost(0, true); } initialPosts.forEach((post) => { handleSpoilers(post); }); processAllPosts(); postObserver.observe(document, {childList: true, subtree: true}); } // New post observer const postObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { const isPost = node.classList.contains("postCell"); const isHoverPost = node.classList.contains("quoteTooltip"); const isInlineQuote = node.classList.contains("inlineQuote"); if (isPost) { if (settings.findNextThread && !threadsClosed) { const totalPostCount = document.querySelector("#postCount").innerText; if (totalPostCount >= THREAD_LOCKED_AT) { threadsClosed = true; addNextThreadFakePost(); } } handleSpoilers(node); const id = postId(node); unsafeWindow.posting.idsRelation[id].forEach((innerPost) => { processAllPostsById(id); }); node.querySelectorAll(".quoteLink").forEach((quoteLink) => { const quotedId = quoteLink.innerText.substring(2); const quotedPost = document.getElementById(quotedId); if (quotedPost) { processSinglePost(quotedPost); } }); } else if (isHoverPost || isInlineQuote) { handleSpoilers(node); processSinglePost(node); } } } } }); const processSinglePost = function(post) { const id = postId(post); if (!id) return; const isNice = isNiceId(id) || isNicePost(post); handlePost(post, isNice); } const processAllPosts = function() { for (const id in unsafeWindow.posting.idsRelation) { processAllPostsById(id); } document.querySelectorAll(".inlineQuote").forEach((inlineQuote) => { processSinglePost(inlineQuote); }); const hoverPost = document.querySelector(".quoteTooltip"); if (hoverPost) { processSinglePost(hoverPost); } } const processAllPostsById = function(id) { const innerPostsById = unsafeWindow.posting.idsRelation[id]; let isNice = isNiceId(id); for (const innerPost of innerPostsById) { const post = innerPost.parentElement; if (!isNice) { isNice = isNicePost(post); if (isNice) break; } } innerPostsById.forEach(innerPost => handlePost(innerPost.parentElement, isNice)); } const isNiceId = function(id) { if (!settings.enabled) return true; if (!id) return false; if (manualBypass[id]) return true; const innerPostsById = unsafeWindow.posting.idsRelation[id]; const isOp = innerPostsById.some(innerPost => innerPost.parentElement.classList.contains("opCell")); if (isOp) return true; const idAboveThreshold = innerPostsById.length >= settings.postThreshold; if (idAboveThreshold) return true; return false; } const isNicePost = function(post) { const postIsByYou = DISABLE_YOU_BYPASS ? false : post.querySelector(".youName"); if (postIsByYou) return true; const aboveBlThreshold = post.querySelectorAll(".postInfo > .panelBacklinks > a")?.length >= settings.backlinkThreshold; if (aboveBlThreshold) return true; if (settings.experimental) { const images = post.querySelectorAll(".uploadCell img:not(.imgExpanded)"); const noImages = images.length === 0; if (noImages) return true; const multipleImages = images.length > 1; if (multipleImages) return true; const hasFunImage = Array.from(images).some((image) => { const spoilerImage = image.getAttribute("data-spoiler") === "true" if (spoilerImage) return true; const format = image?.parentElement?.href?.split("/")?.[4]?.split(".")?.[1]?.toUpperCase(); if (format) { const notRudeImage = !RUDE_FORMATS.includes(format); if (notRudeImage) return true; } return false; }); if (hasFunImage) return true; const hasFunText = post.querySelector(".doomText, .moeText, .redText, .pinkText, .diceRoll, .echoText"); if (hasFunText) return true; } return false; } const isRudeId = function(id) { const innerPostsById = unsafeWindow.posting.idsRelation[id]; let rudeCounter = 3; for (const innerPost of innerPostsById) { const images = innerPost.querySelectorAll(".uploadCell img:not(.imgExpanded)"); if (images.length !== 1) --rudeCounter; if (rudeCounter <= 0) return false; } return true; } const handlePost = function(post, isNice) { let botBanButton = post.querySelector(".botBanButton"); let bypassButton = post.querySelector(".bypassButton"); if (isNice) { unblurPost(post); if (bypassButton) { bypassButton.style.display = "none"; } if (botBanButton) { botBanButton.style.display = "none"; } } else { blurPost(post); const isRude = isRudeId(postId(post)); if (bypassButton) { bypassButton.style.display = "inline"; if (isRude) { bypassButton.style.border = "1px solid red"; } } else { bypassButton = bypassButtonForPost(post); post.querySelector(".postInfo.title").appendChild(bypassButton); } if (settings.showBotBanButton) { if (botBanButton) { botBanButton.style.display = "block"; } else { if (isRude) { botBanButton = addBotBanButtonToPost(post); } } } } } const handleSpoilers = function(post) { const spoilers = post.querySelectorAll("img[src*='spoiler'], img[data-spoiler]"); if (!defaultSpoilerSrc) { defaultSpoilerSrc = spoilers[0]?.src; } spoilers.forEach(spoiler => { spoiler.setAttribute("data-spoiler", true); if (settings.revealSpoilers) { const fileName = spoiler.parentElement.href.split("/")[4].split(".")[0]; spoiler.src = `/.media/t_${fileName}`; spoiler.style.border = SPOILER_BORDER; } else { spoiler.src = defaultSpoilerSrc; spoiler.style.border = "0"; } }); } const blurPost = function(post) { post.style.display = settings.hideFiltered ? "none" : "block"; post.querySelectorAll("img").forEach((img) => { img.style.filter = `blur(${settings.blurStrength}px)`; }); } const unblurPost = function(post) { post.style.display = "block"; post.querySelectorAll("img").forEach((img) => { img.style.filter = ""; }); } const loadMenu = function() { document.querySelector(".bagToolbar")?.remove(); // Toolbar container const toolbar = document.createElement("div"); document.querySelector("body").appendChild(toolbar); toolbar.className = "bagToolbar"; toolbar.style.bottom = "0px"; toolbar.style.color = "var(--navbar-text-color)"; toolbar.style.display = "flex"; toolbar.style.gap = "1px"; toolbar.style.right = "0px"; toolbar.style.padding = "1px"; toolbar.style.position = "fixed"; // Toolbar contents container const toolbarContents = document.createElement("div"); toolbar.appendChild(toolbarContents); toolbarContents.style.backgroundColor = "var(--navbar-text-color)"; toolbarContents.style.border = "1px solid var(--navbar-text-color)"; toolbarContents.style.display = menuVisible ? "flex" : "none"; toolbarContents.style.flexDirection = "column"; toolbarContents.style.gap = "1px"; // Tabs container const tabs = document.createElement("div"); tabs.style.display = "flex"; tabs.style.gap = "1px"; const generalTab = makeTab("General"); const generalTabContainer = makeTabContainer("General"); tabs.appendChild(generalTab); toolbarContents.appendChild(generalTabContainer); // Thread subject input const subjectContainer = container(); generalTabContainer.appendChild(subjectContainer); const subjectLabel = label("Thread Subject"); subjectContainer.append(subjectLabel); const subjectInput = input(settings.threadSubject); subjectInput.className = "subjectInput"; subjectInput.size = 10; subjectContainer.appendChild(subjectInput); subjectInput.onchange = () => { settings.threadSubject = subjectInput.value; setSetting("bag_threadSubject", settings.threadSubject); const threadTitle = document.querySelector("div.opHead > span.labelSubject").innerText.toUpperCase(); const titleSetting = settings?.threadSubject?.toUpperCase() ?? ""; if (threadTitle.includes(titleSetting)) { subjectInput.style.backgroundColor = "white"; subjectMatched(); } else { subjectInput.style.backgroundColor = BAD_COLOR; postObserver.disconnect(); } } // Reveal spoilers checkbox const revealContainer = container(); generalTabContainer.appendChild(revealContainer); const revealLabel = label("Reveal Spoilers"); revealContainer.appendChild(revealLabel); const revealCheckbox = checkbox(settings.revealSpoilers); revealContainer.appendChild(revealCheckbox); revealCheckbox.onchange = () => { settings.revealSpoilers = revealCheckbox.checked; setSetting("bag_revealSpoilers", settings.revealSpoilers); document.querySelectorAll(".postCell").forEach(post => handleSpoilers(post)); }; // Next thread checkbox const nextThreadContainer = container(); generalTabContainer.appendChild(nextThreadContainer); const nextThreadLabel = label("Find Next Thread"); nextThreadContainer.appendChild(nextThreadLabel); const nextThreadCheckbox = checkbox(settings.findNextThread); nextThreadContainer.appendChild(nextThreadCheckbox); nextThreadCheckbox.onchange = () => { settings.findNextThread = nextThreadCheckbox.checked; setSetting("bag_findNextThread", settings.findNextThread); }; const filterTab = makeTab("Filter"); const filterTabContainer = makeTabContainer("Filter"); tabs.appendChild(filterTab); toolbarContents.appendChild(filterTabContainer); // Enable checkbox const enableContainer = container(); filterTabContainer.appendChild(enableContainer); const enableLabel = label("Enable Filter"); enableContainer.appendChild(enableLabel); const enableCheckbox = checkbox(settings.enabled); enableContainer.appendChild(enableCheckbox); enableCheckbox.onchange = () => { settings.enabled = enableCheckbox.checked; unsafeWindow.localStorage.setItem("bag_enabled", settings.enabled); if (settings.enabled) { processAllPosts(); postObserver.observe(document, {childList: true, subtree: true}); } else { postObserver.disconnect(); processAllPosts(); } }; // Post threshold input const thresholdContainer = container(); filterTabContainer.appendChild(thresholdContainer); const thresholdLabel = label("Post Threshold"); thresholdContainer.appendChild(thresholdLabel); const thresholdInput = input(settings.postThreshold); thresholdContainer.appendChild(thresholdInput); thresholdInput.onchange = () => { settings.postThreshold = thresholdInput.value; unsafeWindow.localStorage.setItem("bag_postThreshold", settings.postThreshold); processAllPosts(); }; // Backlink threshold input const blThresholdContainer = container(); filterTabContainer.appendChild(blThresholdContainer); const blThresholdLabel = label("Backlink Threshold"); blThresholdContainer.appendChild(blThresholdLabel); const blThresholdInput = input(settings.backlinkThreshold); blThresholdContainer.appendChild(blThresholdInput); blThresholdInput.onchange = () => { settings.backlinkThreshold = blThresholdInput.value; setSetting("bag_backlinkThreshold", settings.backlinkThreshold); processAllPosts(); }; // Blur input const blurContainer = container(); filterTabContainer.appendChild(blurContainer); const blurLabel = label("Blur Strength"); blurContainer.appendChild(blurLabel); const blurInput = input(settings.blurStrength); blurContainer.appendChild(blurInput); blurInput.onchange = () => { settings.blurStrength = blurInput.value; unsafeWindow.localStorage.setItem("bag_blurStrength", settings.blurStrength); processAllPosts(); }; // Experimental checkbox const experimentalContaner = container(); filterTabContainer.appendChild(experimentalContaner); const experimentalLabel = label("Experimental Heuristics"); experimentalContaner.appendChild(experimentalLabel); const experimentalCheckbox = checkbox(settings.experimental); experimentalContaner.appendChild(experimentalCheckbox); experimentalCheckbox.onchange = () => { settings.experimental = experimentalCheckbox.checked; unsafeWindow.localStorage.setItem("bag_experimental", settings.experimental); if (!settings.experimental) { document.querySelectorAll('.innerPost').forEach(innerPost => { innerPost.style.borderRight = "1px solid var(--horizon-sep-color)"; }); document.querySelectorAll(".bypassButton").forEach(bypassButton => { bypassButton.style.border = "1px solid var(--horizon-sep-color)"; }); } processAllPosts(); }; // Hide filtered checkbox const hideContainer = container(); filterTabContainer.appendChild(hideContainer); const hideLabel = label("Hide Filtered"); hideContainer.appendChild(hideLabel); const hideCheckbox = checkbox(settings.hideFiltered); hideContainer.appendChild(hideCheckbox); hideCheckbox.onchange = () => { settings.hideFiltered = hideCheckbox.checked; unsafeWindow.localStorage.setItem("bag_hideFiltered", settings.hideFiltered); processAllPosts(); }; const jannyTab = makeTab("Janny"); const jannyTabContainer = makeTabContainer("Janny"); tabs.appendChild(jannyTab); toolbarContents.appendChild(jannyTabContainer); // Bot ban checkbox const botBanContainer = container(); jannyTabContainer.appendChild(botBanContainer); const botBanLabel = label("Bot Ban Button"); botBanContainer.appendChild(botBanLabel); const botBanCheckbox = checkbox(settings.showBotBanButton); botBanContainer.appendChild(botBanCheckbox); botBanCheckbox.onchange = () => { const checked = botBanCheckbox.checked; if (checked) { if (!isJanny()) { alert(NOT_A_JANNY); botBanCheckbox.checked = false; return; } if (!confirm(BOT_BAN_BUTTON_WARNING)) { botBanCheckbox.checked = false; return; } processAllPosts(); } else { document.querySelectorAll(".botBanButton").forEach((button) => { button.style.display = "none"; }); } settings.botBanCheckbox = checked; setSetting("bag_showBotBanButton", settings.botBanCheckbox); } // Debug tools if (DEBUG_TOOLS_VISIBLE) { const debugTab = makeTab("Debug"); const debugTabContainer = makeTabContainer("Debug"); tabs.appendChild(debugTab); toolbarContents.appendChild(debugTabContainer); const fakePostButton = button(); debugTabContainer.appendChild(fakePostButton); fakePostButton.innerText = "Test Fake Post"; fakePostButton.style.backgroundColor = "var(--background-color)"; fakePostButton.onclick = () => { const url = `${URL_PREFIX}/res/1289960.html` addFakePost(`fake post test\r\n${url}`); } const triggerThreadCheckButton = button(); debugTabContainer.appendChild(triggerThreadCheckButton); triggerThreadCheckButton.innerText = "Test Thread Finder"; triggerThreadCheckButton.style.backgroundColor = "var(--background-color)"; triggerThreadCheckButton.onclick = () => { addNextThreadFakePost(0, true); } } toolbarContents.appendChild(tabs); addToggleButton(toolbar, toolbarContents); } // Post helpers const postId = function(post) { return post?.querySelector('.labelId')?.innerText; } const addFakePost = function(contents) { const outer = document.createElement("div"); document.querySelector(".divPosts").appendChild(outer); outer.className = "fakePost"; outer.style.marginBottom = "0.25em"; const inner = document.createElement("div"); outer.appendChild(inner); inner.className = "innerPost"; const message = document.createElement("div"); inner.appendChild(message); message.className = "divMessage"; message.innerHTML = contents; return inner; } const addNextThreadFakePost = function(initialQueryDelay, includeAutoSage) { document.querySelector(".nextThread")?.remove(); const fakePost = addFakePost(`Searching for next ${settings.threadSubject} thread...`); fakePost.classList.add("nextThread"); const fakePostMessage = document.querySelector(".nextThread .divMessage"); const delay = FORCE_NEXT_THREAD_FAIL ? 500 : 30000; setTimeout(async () => { const found = FORCE_NEXT_THREAD_FAIL ? false : await queryNextThread(fakePost, fakePostMessage, includeAutoSage); if (!found) { fakePostMessage.innerHTML += `\r\nThread not found, retrying in 30s`; let retryCount = 8; const interval = setInterval(async () => { if (retryCount-- < 0) { clearInterval(interval); fakePostMessage.innerHTML += "\r\nNEXT THREAD NOT FOUND" fakePost.style.border = "5px solid red"; return; } const retryFound = await queryNextThread(fakePost, fakePostMessage, includeAutoSage); if (retryFound) { clearInterval(interval); } else { fakePostMessage.innerHTML += `\r\nThread not found, retrying in 30s`; } }, delay); } }, initialQueryDelay ?? 60000); } // returns true if no more retries should be attempted const queryNextThread = async function(fakePost, fakePostMessage, includeAutoSage) { // Try to fix issues people were having where fakePostMessage was undefined even with the fake post present. // Not sure what the actual cause is, haven't been able to replicate if (!fakePost) fakePost = document.querySelector(".nextThread"); if (!fakePostMessage) fakePostMessage = document.querySelector(".nextThread .divMessage"); const catalogUrl = barchiveToV(`${URL_PREFIX}/catalog.json`); unsafeWindow.console.log("searching for next thread", catalogUrl); const catalog = FORCE_NEXT_THREAD_FAIL ? await mockEmptyCatalogResponse() : await fetch(catalogUrl); if (catalog.ok) { const threads = await catalog.json(); for (const thread of threads) { const notAutoSage = includeAutoSage || !thread.autoSage; if (notAutoSage && thread.subject?.includes(settings.threadSubject)) { const url = barchiveToV(`${URL_PREFIX}/res/${thread.threadId}.html`); fakePostMessage.innerHTML = `${thread.subject} [${thread.postCount ?? 1} posts]:\r\n${url}`; fakePost.style.border = "5px solid green"; return true; } } return false; } else { fakePostMessage.innerHTML = "ERROR WHILE LOOKING FOR NEXT THREAD"; fakePost.style.border = "5px solid red"; return true; } } const barchiveToV = function(url) { return url.replace("barchive", "v"); } // LocalStorage Helpers const loadSettings = function() { // State manualBypass = getManualBypass(); settings.activeTab = getStringSetting("bag_activeTab", "General"); // General settings settings.threadSubject = getStringSetting("bag_threadSubject", "/bag/"); settings.revealSpoilers = getBoolSetting("bag_revealSpoilers", false); settings.findNextThread = getBoolSetting("bag_findNextThread", true); // Filter settings settings.enabled = getBoolSetting("bag_enabled", true); settings.postThreshold = getIntSetting("bag_postThreshold", 4); settings.backlinkThreshold = getIntSetting("bag_backlinkThreshold", 3); settings.blurStrength = getIntSetting("bag_blurStrength", 10); settings.experimental = getBoolSetting("bag_experimental", true); settings.hideFiltered = getBoolSetting("bag_hideFiltered", false); // Janny Settings settings.showBotBanButton = getBoolSetting("bag_showBotBanButton", false); } function setSetting(name, value) { unsafeWindow.localStorage.setItem(name, value); } function getSetting(name) { return unsafeWindow.localStorage.getItem(name); } function getBoolSetting(name, defaultValue) { const value = getSetting(name); if (value === null) return defaultValue; return value == "true"; } function getIntSetting(name, defaultValue) { const value = getSetting(name); if (value === null) return defaultValue; return parseInt(value); } function getStringSetting(name, defaultValue) { const value = getSetting(name); if (value === null) return defaultValue; return value } function getManualBypass() { const bypassVar = `bag_bypass_${unsafeWindow.api.threadId}`; const bp = getSetting(bypassVar); return (!bp) ? {} : JSON.parse(bp); } function setManualBypass() { const bypassVar = `bag_bypass_${unsafeWindow.api.threadId}`; const bypassData = JSON.stringify(manualBypass); unsafeWindow.localStorage.setItem(bypassVar, bypassData); } // HTML Helpers function container() { const container = document.createElement("div"); container.style.alignItems = "center"; container.style.backgroundColor = "var(--background-color)"; container.style.display = "flex"; container.style.gap = "0.25rem"; container.style.justifyContent = "space-between"; container.style.padding = "0.25rem"; return container; } function label(text) { const label = document.createElement("div"); label.innerText = text; label.style.color = "white"; return label; } function checkbox(initialValue) { const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.style.cursor = "pointer"; checkbox.checked = initialValue; return checkbox; } function input(initialValue) { const input = document.createElement("input"); input.size = 4; input.value = initialValue; input.style.border = "1px solid gray"; return input; } function button() { const button = document.createElement("div"); button.style.alignItems = "center"; button.style.color = "var(--link-color)"; button.style.cursor = "pointer"; button.style.display = "flex"; button.style.padding = "0.25rem 0.75rem"; button.style.userSelect = "none"; return button; } function bypassButtonForPost(post) { const id = postId(post); if (!id) return; const border = isRudeId(id) ? "1px solid red" : "1px solid var(--horizon-sep-color)"; const bypassButton = button(); bypassButton.className = "bypassButton"; bypassButton.innerText = "+"; bypassButton.style.display = "inline"; bypassButton.style.marginLeft = "auto"; bypassButton.style.border = border; bypassButton.onclick = () => { bypassButton.style.display = "none"; manualBypass[id] = true; setManualBypass(); processSinglePost(post); processAllPostsById(id); }; return bypassButton; } function addBotBanButtonToPost(post) { const innerPost = post.querySelector(".innerPost"); // Janny tools let tools = post.querySelector(".jannyTools"); if (!tools) { tools = document.createElement("div"); tools.className = "jannyTools"; innerPost.appendChild(tools); tools.style.display = "flex"; tools.style.paddingTop = "0.25rem"; tools.style.gap = "1rem"; tools.style.justifyContent = "flex-end"; tools.style.width = "100%"; } // Bot ban button let botBanButton = post.querySelector(".botBanButton"); if (!botBanButton) { botBanButton = document.createElement("div"); botBanButton.className = "botBanButton"; tools.appendChild(botBanButton); botBanButton.innerText = "Bot Ban"; botBanButton.style.border = "1px solid red"; botBanButton.style.cursor = "pointer"; botBanButton.style.margin = "0"; botBanButton.style.padding = "0.25rem"; botBanButton.onclick = () => { const postId = innerPost.querySelector("a.linkQuote").innerText; const dummy = document.createElement("div"); postingMenu.applySingleBan( "", 3, BOT_BAN_REASON, false, 0, "3d", false, true, "v", api.threadId, postId, innerPost, dummy ); } } return botBanButton; } function addToggleButton(toolbar, toolbarContents) { const toggleButton = button(); toolbar.appendChild(toggleButton); toggleButton.innerText = "<<" toggleButton.style.alignSelf = "flex-end"; toggleButton.style.backgroundColor = "var(--background-color)"; toggleButton.style.border = "1px solid var(--navbar-text-color)"; toggleButton.onclick = () => { menuVisible = !menuVisible; toolbarContents.style.display = menuVisible ? "flex" : "none"; toggleButton.innerText = menuVisible ? ">>" : "<<"; } } function makeTab(tabName) { const isActive = settings.activeTab === tabName; const tab = document.createElement("div"); tab.className = "bagTab" tab.style.backgroundColor = "var(--background-color)"; tab.style.color = isActive ? ACTIVE_COLOR : "white"; tab.style.cursor = "pointer"; tab.style.flexGrow = "1"; tab.style.padding = "0.25rem 0.75rem"; tab.innerText = tabName; tab.onclick = () => { settings.activeTab = tabName; setSetting("bag_activeTab", settings.activeTab); // Tab document.querySelectorAll(".bagTab").forEach((tab) => { tab.style.color = "white"; }); tab.style.color = ACTIVE_COLOR; // Tab container document.querySelectorAll(".bagTabContainer").forEach((tabContainer) => { tabContainer.style.display = "none"; }); document.querySelector(`.bagTabContainer[data-tab="${tabName}"]`).style.display = "flex"; }; return tab; } function makeTabContainer(tabName) { const isActive = settings.activeTab === tabName; const tabContainer = document.createElement("div"); tabContainer.className = "bagTabContainer"; tabContainer.setAttribute("data-tab", tabName) tabContainer.style.display = isActive ? "flex" : "none"; tabContainer.style.flexDirection = "column" tabContainer.style.gap = "1px"; return tabContainer; } // Misc function isJanny() { document.querySelector(".extraMenu").querySelectorAll("li").forEach((item) => { if (item.innerText === "Ban") return true; }); return false; }; // Debug/Test helpers function mockEmptyCatalogResponse() { return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }