diff --git a/README.md b/README.md index 770e4fa..3262cb5 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ -# Steam Workshop Janitor +# Steam Janitor _A userscript that makes browsing through thousands of mods viable_ -## [Install 0.0.1](https://github.com/Jetsparrow/steam-workshop-janitor/raw/main/steam-workshop-janitor.user.js) +## [Install 0.0.2](https://github.com/Jetsparrow/steam-janitor/raw/main/steam-janitor.user.js) +_Requires a userscript extension for your browser, like [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=en)_ -**N.B.: this is a very crude, hacked together proof of concept. It doesn't even have a way to undo the filtering yet.** +**N.B.: this is a very crude alpha. Developed and tested in Google Chrome** ## Filter mods -Adds a little "filter mod" X button to any workshop browse page. Use it, and you will no longer see the hundreds of miscellaneous translations to laguages you don't know, obsolete mods, etc. +Adds a little "hide" button to any workshop browse page. Use it, and you will no longer see the hundreds of miscellaneous translations to laguages you don't know, obsolete mods, or gachimuchi memes. + +The userscripts adds a "filter" checkbox to the top paging controls, allowing you to switch between the filtered and unfiltered view. ## Endless scrolling -After the filter runs its course, you will be left with barely any mods. Wndless scrolling fixes that. +After the filter runs its course, you will be left with barely any mods. Endless scrolling fixes that. diff --git a/steam-janitor.user.js b/steam-janitor.user.js new file mode 100644 index 0000000..ea09c88 --- /dev/null +++ b/steam-janitor.user.js @@ -0,0 +1,192 @@ +// ==UserScript== +// @name Steam Janitor +// @description Hide unwanted user content in browse view, endless scrolling +// @match *://*.steamcommunity.com/workshop/browse/* +// @run-at document-end +// @version 0.0.2 +// @grant GM_setValue +// @grant GM_getValue +// @grant GM_listValues +// @updateURL https://raw.githubusercontent.com/Jetsparrow/steam-janitor/main/steam-janitor.js +// @downloadURL https://raw.githubusercontent.com/Jetsparrow/steam-janitor/main/steam-janitor.js +// ==/UserScript== + +const addGlobalStyle = (doc, css) => { + let head = doc.getElementsByTagName('head')[0]; + if (!head) return null; + let style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = css; + head.appendChild(style); + return style; +} + +const htmlToElement = (doc, html) => { + var template = doc.createElement('template'); + template.innerHTML = html.trim(); + return template.content.firstChild; +} + +const onVisible = (element, callback) => { + const observer = new IntersectionObserver( + (entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + callback(); + observer.unobserve(entry.target); + } + }); + }, + { rootMargin: "0px 0px 200px 0px" } + ); + + observer.observe(element); +}; +const selectors = { + PAGING_INFO: ".workshopBrowsePagingWithBG", + ITEM_CONTAINER: ".workshopBrowseItems .workshopItemPreviewHolder", + ITEMS_CONTAINER: ".workshopBrowseItems", + ITEMS_HOVERS: ".workshopBrowseItems script", + NEXT_BUTTON: ".workshopBrowsePagingControls .pagebtn:last-child", + PAGINATOR: ".workshopBrowsePagingControls", + FOOTER: ".workshopBrowsePaging" +}; + +const unhiddenClass = "janitorItem"; +const hiddenFilteredClass = "janitorItemHidden"; +const hiddenUnfilteredClass = "janitorItemHiddenUnfiltered"; +const hideButtonClass = "janitorHideButton"; +const showButtonClass = "janitorShowButton"; + +const janitorCss = ` +.${hiddenFilteredClass} { display:none !important; } +.${hiddenUnfilteredClass} {opacity: 0.25;} +.${hiddenUnfilteredClass} .${showButtonClass} { display:inline !important; } +.${hiddenUnfilteredClass} .${hideButtonClass} { display:none !important; } +.${unhiddenClass} .${showButtonClass} { display:none !important; } +.${unhiddenClass} .${hideButtonClass} { display:inline !important; } +`; + +const defaultModData = () => { + let d = new Object(); + d.hide = false; + return d; +} +const loadModData = (modId) => { + var j = GM_getValue("modid:" + modId, ""); + return j == "" ? defaultModData() : JSON.parse(j); +} +const saveModData = (modId, data) => GM_setValue("modid:" + modId, JSON.stringify(data)); + +const updateHiddenClass = (doc, modId) => { + var container = doc.getElementById(modId)?.parentElement?.parentElement; + if (!container) return; + const d = loadModData(modId); + container.classList.remove(unhiddenClass); + container.classList.remove(hiddenFilteredClass); + container.classList.remove(hiddenUnfilteredClass); + + if (!d.hide) container.classList.add(unhiddenClass); + else if (window.janitorFilterEnabled) container.classList.add(hiddenFilteredClass); + else container.classList.add(hiddenUnfilteredClass); +} + +const setHidden = (doc, modId, isHidden) => { + var d = loadModData(modId); + d.hide = isHidden; + saveModData(modId, d); + updateHiddenClass(doc, modId); +} + +const addHideButtons = (doc, container, id) => { + const hide = htmlToElement(doc, `hide`) + hide.onclick = () => {setHidden(document, id, true);}; + container.prepend(hide); + + const show = htmlToElement(doc, `show`) + show.onclick = () => {setHidden(document, id, false);}; + container.prepend(show); +} + +const processContainers = (doc) => { + for (var el of doc.querySelectorAll(selectors.ITEM_CONTAINER)){ + const container = el.parentElement.parentElement; + const id = el.id; + updateHiddenClass(doc, id); + + if (container.janitorButtonsAdded) continue; + container.janitorButtonsAdded = true; + addHideButtons(doc, container, id); + } +} + +const loadNextPage = (url) => { + fetch(url, { credentials: "same-origin" }) + .then(response => response.text()) + .then(html => { + const parser = new DOMParser(); + const newDoc = parser.parseFromString(html, "text/html"); + processContainers(newDoc); + + const newMods = newDoc.querySelectorAll(selectors.ITEM_CONTAINER); + const modContainer = document.querySelector(selectors.ITEMS_CONTAINER); + for (const mod of newMods) { + const container = mod.parentElement.parentElement; + modContainer.appendChild(container); + } + + const scripts = newDoc.querySelectorAll(selectors.ITEMS_HOVERS); + for (const newScript of scripts){ + const matches = newScript.innerHTML.match(/(sharedfile_\d+)/); + if (matches.length < 1) continue; + const data = loadModData(matches[0]); + if (data.hide) continue; + eval("try{ "+ newScript.innerHTML + " } catch {} "); + } + + const nextUrl = newDoc.querySelector(selectors.NEXT_BUTTON)?.getAttribute("href"); + const footer = document.getElementById("footer"); + if (nextUrl) onVisible(footer, loadNextPage.bind(null, nextUrl)); + window.history.pushState("", "", url); + }); +}; + +function toggleFilter(checkbox) { + window.janitorFilterEnabled = checkbox.checked; + GM_setValue("janitorFilterEnabled", checkbox.checked); + const doc = checkbox.ownerDocument; + processContainers(doc); +} + +(() => { + const load = () => { + + console.log(GM_listValues()); + + addGlobalStyle(document, janitorCss); + + const filterToggleRoot = document.querySelector(selectors.PAGING_INFO); + const toggle = htmlToElement(document, ` +